Files
Server/src/engine/gameManager/StaticBuildingParity.java
T
MagicBotandClaude Opus 5.5 173955651c 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>
2026-10-27 22:46:31 -04:00

262 lines
11 KiB
Java

// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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);
}
}