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>
262 lines
11 KiB
Java
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);
|
|
}
|
|
|
|
}
|