From 173955651cf83712a5fca8e40349441dd669959c Mon Sep 17 00:00:00 2001 From: MagicBot Date: Tue, 27 Oct 2026 22:46:31 -0400 Subject: [PATCH] Build static buildings from zone props and compare them with the DB at boot Building.createStaticBuilding builds a static building from a CZONE prop: mesh, name override, yaw pi - y_rot, scale 1, CITY rank and funds, protection NPC, floor/level as the DB stores them. Top-level props sit on the terrain (live servers ignored their CZONE Y); children keep their local CZONE Y and take their world location from parent x local. MB_STATIC_BUILDINGS=parity builds every prop after the DB load, matches it to the DB building by zone, mesh and local x/z, logs each difference class and discards the JSON buildings; they take no static IDs. Any other value keeps today's load. The entry must be set on every server. Value changes: none in play (the JSON buildings are discarded). Co-Authored-By: Claude Opus 5.5 --- src/engine/gameManager/ConfigManager.java | 1 + .../gameManager/StaticBuildingParity.java | 261 ++++++++++++++++++ src/engine/objects/Building.java | 64 +++++ src/engine/server/world/WorldServer.java | 5 + 4 files changed, 331 insertions(+) create mode 100644 src/engine/gameManager/StaticBuildingParity.java diff --git a/src/engine/gameManager/ConfigManager.java b/src/engine/gameManager/ConfigManager.java index 3ed271d8..6f031828 100644 --- a/src/engine/gameManager/ConfigManager.java +++ b/src/engine/gameManager/ConfigManager.java @@ -71,6 +71,7 @@ public enum ConfigManager { MB_WORLD_KEYCLONE_MAX, MB_USE_RUINS, MB_RULESET, + MB_STATIC_BUILDINGS, // Mobile AI modifiers MB_AI_CAST_FREQUENCY, diff --git a/src/engine/gameManager/StaticBuildingParity.java b/src/engine/gameManager/StaticBuildingParity.java new file mode 100644 index 00000000..01d1576f --- /dev/null +++ b/src/engine/gameManager/StaticBuildingParity.java @@ -0,0 +1,261 @@ +// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . +// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· +// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ +// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ +// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ +// Magicbane Emulator Project © 2013 - 2026 +// https://www.magicbane.com + +package engine.gameManager; + +import engine.math.Quaternion; +import engine.mbEnums.ZonePropType; +import engine.objects.Building; +import engine.objects.Guild; +import engine.objects.Zone; +import engine.parsers.objects.CZoneProp; +import engine.parsers.objects.ZoneTreeEntry; +import org.pmw.tinylog.Logger; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +// World Boot Plan Task 2.3: builds every CZONE prop of the static zones through Building.createStaticBuilding +// and compares it with the DB building it replaces. The JSON buildings are discarded and take no static IDs. + +public enum StaticBuildingParity { + + STATICBUILDINGPARITY; + + private static final float POSITION_TOLERANCE = 0.01f; + private static final float ROTATION_TOLERANCE = 0.0001f; + private static final int EXAMPLES = 5; + + private static final TreeMap> differences = new TreeMap<>(); + private static int propCount; + private static int matchCount; + private static float maxChildMove; + + public static boolean isEnabled() { + return ConfigManager.MB_STATIC_BUILDINGS.getValue().equalsIgnoreCase("parity"); + } + + // Run after NPCs load, so a DB building's owner resolves to its guild. + public static void run() { + + Zone zone; + ArrayList dbTopLevel = new ArrayList<>(); + ArrayList dbChildren = new ArrayList<>(); + ArrayList dbUnmatched = new ArrayList<>(); + int propIndex; + + for (ZoneTreeEntry treeEntry : ZoneManager.zoneTree) { + + zone = ZoneManager.getZoneByUUID(treeEntry.uid); + + if (zone == null || zone.template == null) + continue; + + dbTopLevel.clear(); + dbChildren.clear(); + + // Mine towers (blueprint 1500100) stay DB objects (Task 2.1, D1); every CZONE prop has blueprint 0. + for (Building building : zone.zoneBuildingSet) { + + if (building.blueprintUUID != 0) + continue; + + if (building.parentBuildingID == 0) + dbTopLevel.add(building); + else + dbChildren.add(building); + } + + propIndex = 0; + + for (CZoneProp prop : zone.template.zone_prop_info) { + compareProp(zone, prop, "prop:" + zone.getObjectUUID() + "/" + propIndex, dbTopLevel, dbChildren); + propIndex++; + } + + dbUnmatched.addAll(dbTopLevel); + dbUnmatched.addAll(dbChildren); + } + + for (Building building : dbUnmatched) + addDifference("DB only", building.getObjectUUID() + " zone " + building.parentZoneUUID + " mesh " + building.meshUUID + + " '" + building.getName() + "' at " + building.statLat + ", " + building.statLon + " parent " + building.parentBuildingID); + + Logger.info("Static building parity: " + propCount + " props, " + matchCount + " matched, max child x/z move from the parent yaw " + maxChildMove); + + for (Map.Entry> entry : differences.entrySet()) { + + Logger.info(" " + entry.getKey() + ": " + entry.getValue().size()); + + for (int i = 0; i < Math.min(EXAMPLES, entry.getValue().size()); i++) + Logger.info(" " + entry.getValue().get(i)); + } + + differences.clear(); + } + + private static void compareProp(Zone zone, CZoneProp prop, String key, ArrayList dbTopLevel, ArrayList dbChildren) { + + Building jsonBuilding; + Building dbBuilding; + Building jsonChild; + Building dbChild; + ArrayList dbChildCandidates = new ArrayList<>(); + String childKey; + float childMove; + int childIndex; + + jsonBuilding = Building.createStaticBuilding(zone, prop, null); + dbBuilding = takeMatch(jsonBuilding, dbTopLevel); + propCount++; + + if (dbBuilding == null) + addDifference("JSON only", key + " mesh " + jsonBuilding.meshUUID + " '" + jsonBuilding.getName() + "'"); + else { + compareBuilding(key, prop, dbBuilding, jsonBuilding); + + if (dbBuilding.getLoc().distance2D(jsonBuilding.getLoc()) > POSITION_TOLERANCE + || Math.abs(dbBuilding.getLoc().y - jsonBuilding.getLoc().y) > POSITION_TOLERANCE) + addDifference("top-level world location", key + " DB " + dbBuilding.getLoc() + " JSON " + jsonBuilding.getLoc()); + + for (Building candidate : dbChildren) + if (candidate.parentBuildingID == dbBuilding.getObjectUUID()) + dbChildCandidates.add(candidate); + } + + childIndex = 0; + + for (CZoneProp childProp : prop.prop_content_props) { + + childKey = key + "/" + childIndex; + jsonChild = Building.createStaticBuilding(zone, childProp, jsonBuilding); + dbChild = takeMatch(jsonChild, dbChildCandidates); + + propCount++; + childIndex++; + + if (dbChild == null) { + addDifference("JSON only", childKey + " mesh " + jsonChild.meshUUID + " '" + jsonChild.getName() + "'"); + continue; + } + + dbChildren.remove(dbChild); + compareBuilding(childKey, childProp, dbChild, jsonChild); + + if (Math.abs(dbChild.getStatAlt() - jsonChild.getStatAlt()) > POSITION_TOLERANCE) + addDifference("child local Y", childKey + " DB " + dbChild.getStatAlt() + " JSON " + jsonChild.getStatAlt()); + + // Today's runAfterLoad adds the child offset without the parent's yaw. + childMove = dbChild.getLoc().distance2D(jsonChild.getLoc()); + + if (childMove > POSITION_TOLERANCE) { + addDifference("child world x/z (parent yaw)", childKey + " moved " + childMove); + maxChildMove = Math.max(maxChildMove, childMove); + } + } + } + + // Same mesh and local x/z; removes and returns the closest DB building, or null. + private static Building takeMatch(Building jsonBuilding, ArrayList candidates) { + + Building found = null; + float foundDistance = Float.MAX_VALUE; + float distance; + + for (Building candidate : candidates) { + + if (candidate.meshUUID != jsonBuilding.meshUUID) + continue; + + distance = Math.max(Math.abs(candidate.statLat - jsonBuilding.statLat), Math.abs(candidate.statLon - jsonBuilding.statLon)); + + if (distance <= POSITION_TOLERANCE && distance < foundDistance) { + found = candidate; + foundDistance = distance; + } + } + + if (found != null) { + candidates.remove(found); + matchCount++; + } + + return found; + } + + private static void compareBuilding(String key, CZoneProp prop, Building dbBuilding, Building jsonBuilding) { + + String prefix = key + " (DB " + dbBuilding.getObjectUUID() + ")"; + String dbGuild = guildName(dbBuilding); + String jsonGuild = ""; + + if (!sameRotation(dbBuilding.getRotation(), jsonBuilding.getRotation())) + addDifference("rotation", prefix + " DB " + rotationText(dbBuilding.getRotation()) + " JSON " + rotationText(jsonBuilding.getRotation())); + + if (!dbBuilding.getMeshScale().equals(jsonBuilding.getMeshScale())) + addDifference("scale", prefix + " DB " + dbBuilding.getMeshScale() + " JSON " + jsonBuilding.getMeshScale()); + + if (dbBuilding.rank != jsonBuilding.rank) + addDifference("rank", prefix + " DB " + dbBuilding.rank + " JSON " + jsonBuilding.rank); + + if (dbBuilding.getStrongboxValue() != jsonBuilding.getStrongboxValue()) + addDifference("strongbox", prefix + " DB " + dbBuilding.getStrongboxValue() + " JSON " + jsonBuilding.getStrongboxValue()); + + if (!dbBuilding.getName().equals(jsonBuilding.getName())) + addDifference("name", prefix + " DB '" + dbBuilding.getName() + "' JSON '" + jsonBuilding.getName() + "'"); + + if (dbBuilding.floor != jsonBuilding.floor || dbBuilding.level != jsonBuilding.level) + addDifference("floor/level", prefix + " DB " + dbBuilding.floor + "/" + dbBuilding.level + " JSON " + jsonBuilding.floor + "/" + jsonBuilding.level); + + if (dbBuilding.protectionState != jsonBuilding.protectionState) + addDifference("protection", prefix + " DB " + dbBuilding.protectionState + " JSON " + jsonBuilding.protectionState); + + if (dbBuilding.isFurniture != jsonBuilding.isFurniture) + addDifference("furniture flag", prefix + " DB " + dbBuilding.isFurniture + " JSON " + jsonBuilding.isFurniture); + + // Owners are wired in 2.4; until then compare the guild the CITY prop's key names. + if (prop.prop_type == ZonePropType.CITY && GuildManager._npcGuildsByKey.containsKey(prop.city_prop_gm_guild)) + jsonGuild = GuildManager._npcGuildsByKey.get(prop.city_prop_gm_guild).getName(); + + if (!dbGuild.equals(jsonGuild)) + addDifference("owner guild", prefix + " DB '" + dbGuild + "' JSON '" + jsonGuild + "'"); + } + + // q and -q are the same rotation. + private static boolean sameRotation(Quaternion a, Quaternion b) { + + boolean same = Math.abs(a.w - b.w) < ROTATION_TOLERANCE && Math.abs(a.x - b.x) < ROTATION_TOLERANCE + && Math.abs(a.y - b.y) < ROTATION_TOLERANCE && Math.abs(a.z - b.z) < ROTATION_TOLERANCE; + boolean negated = Math.abs(a.w + b.w) < ROTATION_TOLERANCE && Math.abs(a.x + b.x) < ROTATION_TOLERANCE + && Math.abs(a.y + b.y) < ROTATION_TOLERANCE && Math.abs(a.z + b.z) < ROTATION_TOLERANCE; + + return same || negated; + } + + private static String rotationText(Quaternion rotation) { + return "(w " + rotation.w + ", x " + rotation.x + ", y " + rotation.y + ", z " + rotation.z + ")"; + } + + private static String guildName(Building building) { + + Guild guild; + + if (building.getOwnerUUID() == 0) + return ""; + + guild = building.getGuild(); + + return guild == null || guild.isEmptyGuild() ? "" : guild.getName(); + } + + private static void addDifference(String differenceClass, String line) { + differences.computeIfAbsent(differenceClass, k -> new ArrayList<>()).add(line); + } + +} diff --git a/src/engine/objects/Building.java b/src/engine/objects/Building.java index 66245a0b..10a1e3e6 100644 --- a/src/engine/objects/Building.java +++ b/src/engine/objects/Building.java @@ -25,6 +25,7 @@ import engine.jobs.DoorCloseJob; import engine.jobs.SiegeSpireWithdrawlJob; import engine.math.Bounds; import engine.math.Quaternion; +import engine.math.Transform; import engine.math.Vector3f; import engine.math.Vector3fImmutable; import engine.mbEnums; @@ -32,6 +33,8 @@ import engine.mbEnums.*; import engine.net.ByteBufferWriter; import engine.net.client.msg.ApplyBuildingEffectMsg; import engine.net.client.msg.UpdateObjectMsg; +import engine.parsers.objects.CZonePlacement; +import engine.parsers.objects.CZoneProp; import engine.server.MBServerStatics; import org.joda.time.DateTime; import org.pmw.tinylog.Logger; @@ -217,6 +220,67 @@ public class Building extends AbstractWorldObject { return building; } + // A static building from a CZONE prop (W4); the caller assigns the ID. Children keep their local CZONE + // position and yaw, as live servers sent them; top-level props sit on the terrain, since live servers + // ignored the CZONE Y of top-level props (World-Boot-Plan findings, Task 2.3). + public static Building createStaticBuilding(Zone parentZone, CZoneProp prop, Building parentBuilding) { + + Building building = new Building(); + CZonePlacement placement = prop.prop_basic_zone; + Vector3fImmutable localLoc = placement.basic_zone_spawn_location; + + building.setStaticWorldObject(true); + building.setObjectTypeMask(MBServerStatics.MASK_BUILDING); + building.gridObjectType = GridObjectType.STATIC; + building.parentZoneUUID = parentZone.getObjectUUID(); + building.name = placement.basic_zone_name_override; + building.meshUUID = placement.basic_zone_template_id; + building.statLat = localLoc.x; + building.statAlt = parentBuilding == null ? 0 : localLoc.y; + building.statLon = localLoc.z; + building.meshScale = new Vector3f(1.0f, 1.0f, 1.0f); + + // CZONE y_rot is not the client's yaw: live servers sent π − y_rot (specs/WorldPlacement.md §3, W8). + building.setRotation(Quaternion.fromYaw((float) (Math.PI - placement.basic_zone_y_rot))); + + building.protectionState = ProtectionState.NPC; + building.maxGold = 15000000; + + if (prop.prop_type == ZonePropType.CITY) { + building.rank = prop.city_prop_asset_rank; + building._strongboxValue = prop.city_prop_asset_funds; + } + + // Same swap as the DB rows; runAfterLoad's region lookup expects it. + building.floor = placement.basic_zone_level_number; + building.level = placement.basic_zone_room_number; + + building.isFurniture = parentBuilding != null; + building.parentBuildingID = parentBuilding == null ? 0 : parentBuilding.getObjectUUID(); + building.setLoc(getStaticWorldLocation(building, parentZone, parentBuilding)); + + return building; + } + + // Top-level: zone position on the terrain. Child: parent × local, so the parent's yaw turns the offset. + private static Vector3fImmutable getStaticWorldLocation(Building building, Zone parentZone, Building parentBuilding) { + + Vector3fImmutable groundLoc; + Transform parentTransform; + Transform localTransform; + + if (parentBuilding == null) { + groundLoc = new Vector3fImmutable(building.statLat + parentZone.absX, 0, building.statLon + parentZone.absZ); + return groundLoc.setY(Terrain.getWorldHeight(groundLoc) + building.statAlt); + } + + parentTransform = new Transform(parentBuilding.getLoc(), parentBuilding.getRotation(), parentBuilding.meshScale); + localTransform = new Transform(new Vector3fImmutable(building.statLat, building.statAlt, building.statLon), + building.getRotation(), building.meshScale); + + return Transform.mult(parentTransform, localTransform).location; + } + // Writes a new player building once and loads it into the simulation; returns null if the insert failed. public static Building persistBuilding(Building building) { diff --git a/src/engine/server/world/WorldServer.java b/src/engine/server/world/WorldServer.java index c30cbaed..c61badeb 100644 --- a/src/engine/server/world/WorldServer.java +++ b/src/engine/server/world/WorldServer.java @@ -578,6 +578,11 @@ public class WorldServer { Logger.info("Loading all Mobiles"); DbManager.MobQueries.GET_ALL_MOBS(); + if (StaticBuildingParity.isEnabled()) { + Logger.info("Comparing static buildings with the zone JSON"); + StaticBuildingParity.run(); + } + StaticManifest.writeManifest(worldMapID); StaticManifest.logOrphanReferences();