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:
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user