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 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-27 22:46:31 -04:00
1 parent b83cde8141
commit 173955651c
4 files changed
+331

No files matched your search

@@ -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,
@@ -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<String, ArrayList<String>> 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<Building> dbTopLevel = new ArrayList<>();
ArrayList<Building> dbChildren = new ArrayList<>();
ArrayList<Building> 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<String, ArrayList<String>> 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<Building> dbTopLevel, ArrayList<Building> dbChildren) {
Building jsonBuilding;
Building dbBuilding;
Building jsonChild;
Building dbChild;
ArrayList<Building> 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<Building> 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);
}
}
+64
View File
@@ -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) {
+5
View File
@@ -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();