diff --git a/src/engine/parsers/cache/ParityCheck.java b/src/engine/parsers/cache/ParityCheck.java new file mode 100644 index 00000000..25b9425e --- /dev/null +++ b/src/engine/parsers/cache/ParityCheck.java @@ -0,0 +1,118 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.parsers.cache; + +import engine.gameManager.ConfigManager; +import engine.gameManager.DbManager; +import engine.mbEnums; +import engine.objects.AbstractGameObject; +import engine.objects.BaseClass; +import engine.parsers.objects.RuneTemplate; +import org.pmw.tinylog.Logger; + +import java.util.LinkedHashMap; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +// Dev-only check that compares DB-built objects with the cache JSON before a loader is switched. +// Enabled by the optional env var MB_PARITY_CHECK; not in the ConfigManager enum, which requires every entry. + +public class ParityCheck { + + private static final String FLAG_NAME = "MB_PARITY_CHECK"; + private static final LinkedHashMap> comparators = new LinkedHashMap<>(); + + private final String table; + private int comparedCount = 0; + private int differenceCount = 0; + private int missingCount = 0; + + static { + register("static_rune_baseclass", ParityCheck::compareBaseClasses); + } + + private ParityCheck(String table) { + this.table = table; + } + + public static boolean isEnabled() { + return ConfigManager.configMap.getOrDefault(FLAG_NAME, "false").equalsIgnoreCase("true"); + } + + public static void register(String table, Consumer comparator) { + comparators.put(table, comparator); + } + + public static void run() { + + ParityCheck check; + + for (String table : comparators.keySet()) { + check = new ParityCheck(table); + + try { + comparators.get(table).accept(check); + } catch (Exception e) { + Logger.error("Parity: " + table + ": comparator failed: " + e); + } + + Logger.info("Parity: " + table + ": " + check.comparedCount + " compared, " + + check.differenceCount + " differences, " + check.missingCount + " missing in json"); + } + } + + // Returns false when the row has no JSON counterpart, so the comparator can skip its fields. + + public boolean compareRow(int id, Object jsonObject) { + + comparedCount++; + + if (jsonObject == null) { + missingCount++; + Logger.warn("Parity: " + table + "/" + id + ": missing in json"); + return false; + } + + return true; + } + + public void compareField(int id, String field, Object dbValue, Object jsonValue) { + + if (Objects.equals(dbValue, jsonValue)) + return; + + differenceCount++; + Logger.warn("Parity: " + table + "/" + id + "/" + field + ": db=" + dbValue + " json=" + jsonValue); + } + + private static void compareBaseClasses(ParityCheck check) { + + ConcurrentHashMap cache; + BaseClass baseClass; + RuneTemplate rune; + + cache = DbManager.getMap(mbEnums.GameObjectType.BaseClass); + + if (cache == null) { + Logger.warn("Parity: static_rune_baseclass: no BaseClass objects loaded"); + return; + } + + for (AbstractGameObject cached : cache.values()) { + baseClass = (BaseClass) cached; + rune = RuneTemplate._runes.get(baseClass.getObjectUUID()); + + if (!check.compareRow(baseClass.getObjectUUID(), rune)) + continue; + + check.compareField(baseClass.getObjectUUID(), "name", baseClass.getName(), rune.obj_name); + } + } +} diff --git a/src/engine/server/world/WorldServer.java b/src/engine/server/world/WorldServer.java index ffc21748..6466ee63 100644 --- a/src/engine/server/world/WorldServer.java +++ b/src/engine/server/world/WorldServer.java @@ -34,6 +34,7 @@ import engine.objects.*; import engine.parsers.cache.CObjectIndex; import engine.parsers.cache.DeedParser; import engine.parsers.cache.ItemParser; +import engine.parsers.cache.ParityCheck; import engine.parsers.cache.RuneParser; import engine.parsers.objects.DeedTemplate; import engine.parsers.objects.RuneTemplate; @@ -395,6 +396,11 @@ public class WorldServer { mbEnums.RaceType.initRaceTypeTables(); Race.loadAllRaces(); + if (ParityCheck.isEnabled()) { + Logger.info("Running DB/JSON parity check"); + ParityCheck.run(); + } + Logger.info("Loading building slot/stuck location data."); BuildingLocation.loadBuildingLocations();