Load static buildings from the zone JSON and write the 2.4 migration mapping

MB_STATIC_BUILDINGS picks the static building source (World Boot Plan 2.4):
- DB: today's load; static zones keep their tree UIDs.
- PARITY: DB load, then the JSON buildings are built with the IDs a JSON boot
  gives them, compared with the DB rows and discarded. The match runs three
  passes (exact zone/mesh/x/z; hand-moved rows by mesh and name in the zone
  or its parent; rows of dropped twins onto the kept twin) and writes
  maps/<map>.migration.sql, the old UID -> static ID table the migration
  script reads. Every unmapped DB row is logged.
- JSON: static zones take negative IDs, static buildings come from the
  CZONE props, and the DB loads only what is left (player buildings, mine
  towers).
Boot aborts when the mode does not fit the DB: JSON needs the static
obj_zone rows gone, DB and PARITY need them present.

Static IDs, fixed from here on: guilds, then zones in tree order, then
buildings zone by zone in tree order, each top-level prop followed by its
children, in JSON order. Every mode takes the zone IDs, so the manifest of
a PARITY boot equals the JSON boot's. A prop with the same mesh, name,
local position and yaw as an earlier sibling is dropped with its children
and takes no ID; the name keeps Swampstone's four stacked vendor halls.

Static buildings keep the factory's parent x local world location in
runAfterLoad. CITY props are owned by the leader NPC of the DB guild with
the seed guild's name, an interim until static guilds get leaders (2.5/3.2).

Value changes in JSON mode (decided in 2.3 unless noted):
- every static zone and building ID becomes negative (W10);
- children take their CZONE local Y; 9,362 children move up to 160 units
  on the server once the parent yaw applies (the client gets local values);
- Swampstone halls, the Silverlance shop and the Sea Dog's Rest tree follow
  the JSON; owners follow the CITY key (13 props unowned, Junktown owned by
  Greyman's Band); prop:1276/18 rank 0 -> 1;
- stacked twins are gone (7 top-level with their children, ~446 children;
  counts logged per zone at boot).
DB mode: no value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-27 23:37:54 -04:00
1 parent 5a89a14447
commit 4c2648046b
8 files changed
+549 -109

No files matched your search

@@ -44,4 +44,44 @@ public class dbStaticWorldHandler extends dbHandlerBase {
return rowsByValue;
}
// Leader NPC of each DB NPC guild by guild name. Interim owner source for static buildings until the
// static guilds get their own leaders (World Boot Plan 2.5/3.2).
public HashMap<String, Integer> GET_NPC_GUILD_LEADERS() {
HashMap<String, Integer> leadersByName = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `obj_guild`.`name`, `obj_guild`.`leaderUID` FROM `obj_guild` "
+ "INNER JOIN `object` ON `object`.`UID` = `obj_guild`.`leaderUID` WHERE `object`.`type` = 'npc';")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
leadersByName.put(rs.getString("name"), rs.getInt("leaderUID"));
} catch (SQLException e) {
Logger.error(e);
}
return leadersByName;
}
// obj_zone rows built by the import procedures; 0 once the 2.4 migration has run, -1 on error.
public int COUNT_STATIC_ZONE_ROWS() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(*) AS `zones` FROM `obj_zone` WHERE `template` <> 0;")) {
ResultSet rs = preparedStatement.executeQuery();
if (rs.next())
return rs.getInt("zones");
} catch (SQLException e) {
Logger.error(e);
}
return -1;
}
}
@@ -0,0 +1,191 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.mbEnums.GameObjectType;
import engine.mbEnums.StaticBuildingMode;
import engine.mbEnums.ZonePropType;
import engine.objects.Building;
import engine.objects.Guild;
import engine.objects.Zone;
import engine.parsers.objects.CZonePlacement;
import engine.parsers.objects.CZoneProp;
import engine.parsers.objects.ZoneTreeEntry;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.HashMap;
// World Boot Plan 2.4: static buildings from the CZONE props of the static zones. IDs follow the zones, zone by
// zone in tree order: each top-level prop in JSON order, then its children in JSON order. Never renumber.
public enum StaticBuildingLoader {
STATICBUILDINGLOADER;
private static final float TWIN_POSITION_TOLERANCE = 0.01f;
private static final float TWIN_YAW_TOLERANCE = 0.0001f;
public static StaticBuildingMode getMode() {
return StaticBuildingMode.fromConfig(ConfigManager.MB_STATIC_BUILDINGS.getValue());
}
// addToWorld false (parity) builds every building with ID 0, outside the cache and the world grid; the
// static IDs are still taken, so the manifest matches a JSON boot.
public static ArrayList<StaticBuildingEntry> buildStaticBuildings(boolean addToWorld) {
ArrayList<StaticBuildingEntry> topLevelEntries = new ArrayList<>();
ArrayList<StaticBuildingEntry> zoneEntries = new ArrayList<>();
HashMap<String, Integer> leadersByName;
StaticBuildingEntry entry;
StaticBuildingEntry childEntry;
Zone zone;
CZoneProp prop;
CZoneProp childProp;
int buildingCount = 0;
int twinCount;
int totalTwins = 0;
int ownerMissing = 0;
int ownerUUID;
leadersByName = DbManager.StaticWorldQueries.GET_NPC_GUILD_LEADERS();
for (ZoneTreeEntry treeEntry : ZoneManager.zoneTree) {
zone = ZoneManager.staticZonesByTreeUID.get(treeEntry.uid);
zoneEntries.clear();
twinCount = 0;
for (int propIndex = 0; propIndex < zone.template.zone_prop_info.size(); propIndex++) {
prop = zone.template.zone_prop_info.get(propIndex);
// A dropped twin takes its children with it
if (isStackedTwin(prop, zoneEntries)) {
twinCount += 1 + prop.prop_content_props.size();
continue;
}
ownerUUID = getOwnerUUID(prop, leadersByName);
if (prop.prop_type == ZonePropType.CITY && ownerUUID == 0 && GuildManager._npcGuildsByKey.containsKey(prop.city_prop_gm_guild))
ownerMissing++;
entry = buildEntry(treeEntry.uid, "prop:" + treeEntry.uid + "/" + propIndex, zone, prop, null, ownerUUID, addToWorld);
zoneEntries.add(entry);
topLevelEntries.add(entry);
buildingCount++;
for (int childIndex = 0; childIndex < prop.prop_content_props.size(); childIndex++) {
childProp = prop.prop_content_props.get(childIndex);
if (isStackedTwin(childProp, entry.children)) {
twinCount++;
continue;
}
childEntry = buildEntry(treeEntry.uid, entry.key + "/" + childIndex, zone, childProp, entry, 0, addToWorld);
entry.children.add(childEntry);
buildingCount++;
}
}
if (twinCount > 0)
Logger.info("Static buildings: " + twinCount + " stacked twins dropped in zone " + treeEntry.uid + " (" + treeEntry.name + ")");
totalTwins += twinCount;
}
if (ownerMissing > 0)
Logger.warn("Static buildings: " + ownerMissing + " CITY props name a guild with no DB leader NPC; left unowned");
Logger.info("Static buildings: " + buildingCount + " built, " + totalTwins + " stacked twins dropped");
return topLevelEntries;
}
private static StaticBuildingEntry buildEntry(int zoneTreeUID, String key, Zone zone, CZoneProp prop, StaticBuildingEntry parentEntry,
int ownerUUID, boolean addToWorld) {
StaticBuildingEntry entry = new StaticBuildingEntry();
entry.key = key;
entry.zoneTreeUID = zoneTreeUID;
entry.prop = prop;
entry.parent = parentEntry;
entry.staticID = DbManager.nextStaticID(GameObjectType.Building, key);
entry.building = Building.createStaticBuilding(addToWorld ? entry.staticID : 0, zone, prop,
parentEntry == null ? null : parentEntry.building, ownerUUID);
if (addToWorld) {
DbManager.addToCache(entry.building);
entry.building.runAfterLoad();
}
return entry;
}
// Same mesh, name, local position and yaw as an earlier kept sibling (findings §6, stacked duplicates;
// the name keeps Swampstone's stacked vendor halls).
private static boolean isStackedTwin(CZoneProp prop, ArrayList<StaticBuildingEntry> keptSiblings) {
CZonePlacement placement = prop.prop_basic_zone;
CZonePlacement keptPlacement;
for (StaticBuildingEntry kept : keptSiblings) {
keptPlacement = kept.prop.prop_basic_zone;
if (keptPlacement.basic_zone_template_id != placement.basic_zone_template_id
|| !keptPlacement.basic_zone_name_override.equals(placement.basic_zone_name_override))
continue;
if (Math.abs(keptPlacement.basic_zone_spawn_location.x - placement.basic_zone_spawn_location.x) <= TWIN_POSITION_TOLERANCE
&& Math.abs(keptPlacement.basic_zone_spawn_location.y - placement.basic_zone_spawn_location.y) <= TWIN_POSITION_TOLERANCE
&& Math.abs(keptPlacement.basic_zone_spawn_location.z - placement.basic_zone_spawn_location.z) <= TWIN_POSITION_TOLERANCE
&& Math.abs(keptPlacement.basic_zone_y_rot - placement.basic_zone_y_rot) <= TWIN_YAW_TOLERANCE)
return true;
}
return false;
}
// The guild the CITY key names, owned through its DB leader NPC as before the switch.
private static int getOwnerUUID(CZoneProp prop, HashMap<String, Integer> leadersByName) {
Guild guild;
Integer leaderUUID;
if (prop.prop_type != ZonePropType.CITY)
return 0;
guild = GuildManager._npcGuildsByKey.get(prop.city_prop_gm_guild);
if (guild == null)
return 0;
leaderUUID = leadersByName.get(guild.getName());
return leaderUUID == null ? 0 : leaderUUID;
}
public static class StaticBuildingEntry {
public String key;
public int zoneTreeUID;
public int staticID;
public CZoneProp prop;
public Building building;
public StaticBuildingEntry parent;
public final ArrayList<StaticBuildingEntry> children = new ArrayList<>();
}
}
+242 -91
View File
@@ -8,161 +8,187 @@
package engine.gameManager;
import engine.gameManager.StaticBuildingLoader.StaticBuildingEntry;
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.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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.
// World Boot Plan 2.3/2.4: builds the static buildings from the CZONE props with the IDs a JSON boot gives them,
// compares them with the DB buildings they replace, and writes maps/<map>.migration.sql, the old UID -> static ID
// mapping the 2.4 migration script re-points references with. The JSON buildings are discarded.
public enum StaticBuildingParity {
STATICBUILDINGPARITY;
private static final String mapPath = ConfigManager.DEFAULT_DATA_DIR + "maps/";
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 int INSERT_BATCH = 500;
private static final String DB_ONLY = "DB only (unmapped, deleted by the migration)";
private static final TreeMap<String, ArrayList<String>> differences = new TreeMap<>();
private static final HashMap<Integer, StaticBuildingEntry> entriesByDbUID = new HashMap<>();
private static final HashSet<StaticBuildingEntry> matchedEntries = new HashSet<>();
private static final HashMap<Integer, ArrayList<Building>> dbChildrenByParent = new HashMap<>();
private static int propCount;
private static int matchCount;
private static int exactCount;
private static int movedCount;
private static int twinCount;
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() {
public static void run(int mapID) {
Zone zone;
ArrayList<Building> dbTopLevel = new ArrayList<>();
ArrayList<Building> dbChildren = new ArrayList<>();
ArrayList<Building> dbUnmatched = new ArrayList<>();
int propIndex;
ArrayList<StaticBuildingEntry> topLevelEntries;
HashMap<Integer, ArrayList<StaticBuildingEntry>> entriesByZone = new HashMap<>();
HashMap<Integer, ArrayList<Building>> dbTopLevelByZone = new HashMap<>();
ArrayList<Building> dbStatic = new ArrayList<>();
ArrayList<Building> dbUnmatchedTop = new ArrayList<>();
Building dbBuilding;
for (ZoneTreeEntry treeEntry : ZoneManager.zoneTree) {
topLevelEntries = StaticBuildingLoader.buildStaticBuildings(false);
zone = ZoneManager.getZoneByUUID(treeEntry.uid);
for (StaticBuildingEntry entry : topLevelEntries) {
entriesByZone.computeIfAbsent(entry.zoneTreeUID, k -> new ArrayList<>()).add(entry);
propCount += 1 + entry.children.size();
}
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.
// Mine towers (blueprint 1500100) stay DB objects (Task 2.1, D1); every CZONE prop has blueprint 0.
for (Zone zone : ZoneManager.staticZonesByTreeUID.values())
for (Building building : zone.zoneBuildingSet) {
if (building.blueprintUUID != 0)
continue;
dbStatic.add(building);
if (building.parentBuildingID == 0)
dbTopLevel.add(building);
dbTopLevelByZone.computeIfAbsent(zone.getObjectUUID(), k -> new ArrayList<>()).add(building);
else
dbChildren.add(building);
dbChildrenByParent.computeIfAbsent(building.parentBuildingID, k -> new ArrayList<>()).add(building);
}
propIndex = 0;
// Pass 1: zone, mesh and local x/z.
for (Map.Entry<Integer, ArrayList<StaticBuildingEntry>> zoneEntries : entriesByZone.entrySet())
for (StaticBuildingEntry entry : zoneEntries.getValue()) {
for (CZoneProp prop : zone.template.zone_prop_info) {
compareProp(zone, prop, "prop:" + zone.getObjectUUID() + "/" + propIndex, dbTopLevel, dbChildren);
propIndex++;
dbBuilding = takeMatch(entry, dbTopLevelByZone.getOrDefault(zoneEntries.getKey(), new ArrayList<>()));
if (dbBuilding == null)
continue;
exactCount++;
mapPair(dbBuilding, entry, true);
}
dbUnmatched.addAll(dbTopLevel);
dbUnmatched.addAll(dbChildren);
for (ArrayList<Building> leftovers : dbTopLevelByZone.values())
dbUnmatchedTop.addAll(leftovers);
// Pass 2: hand-moved DB rows (Swampstone halls, Silverlance shop).
for (Building building : new ArrayList<>(dbUnmatchedTop)) {
StaticBuildingEntry entry = findMovedMatch(building, entriesByZone);
if (entry == null)
continue;
dbUnmatchedTop.remove(building);
movedCount++;
mapPair(building, entry, true);
}
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);
// Pass 3: a DB row of a dropped stacked twin maps to the kept twin.
for (Building building : new ArrayList<>(dbUnmatchedTop)) {
Logger.info("Static building parity: " + propCount + " props, " + matchCount + " matched, max child x/z move from the parent yaw " + maxChildMove);
StaticBuildingEntry entry = findTwin(building, entriesByZone.getOrDefault(building.parentZoneUUID, new ArrayList<>()));
for (Map.Entry<String, ArrayList<String>> entry : differences.entrySet()) {
if (entry == null)
continue;
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));
dbUnmatchedTop.remove(building);
twinCount++;
mapPair(building, entry, false);
}
logResults(dbStatic, topLevelEntries);
writeMigration(mapID, dbStatic);
differences.clear();
entriesByDbUID.clear();
matchedEntries.clear();
dbChildrenByParent.clear();
}
private static void compareProp(Zone zone, CZoneProp prop, String key, ArrayList<Building> dbTopLevel, ArrayList<Building> dbChildren) {
// Maps a DB top-level row and its children. compare is false for twins: their differences are the kept twin's.
private static void mapPair(Building dbBuilding, StaticBuildingEntry entry, boolean compare) {
Building jsonBuilding;
Building dbBuilding;
Building jsonChild;
ArrayList<Building> dbChildren = dbChildrenByParent.getOrDefault(dbBuilding.getObjectUUID(), new ArrayList<>());
ArrayList<Building> dbChildCandidates = new ArrayList<>(dbChildren);
Building dbChild;
ArrayList<Building> dbChildCandidates = new ArrayList<>();
String childKey;
StaticBuildingEntry twin;
float childMove;
int childIndex;
jsonBuilding = Building.createStaticBuilding(zone, prop, null);
dbBuilding = takeMatch(jsonBuilding, dbTopLevel);
propCount++;
entriesByDbUID.put(dbBuilding.getObjectUUID(), entry);
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);
if (compare) {
matchedEntries.add(entry);
compareBuilding(entry, dbBuilding);
}
childIndex = 0;
for (StaticBuildingEntry childEntry : entry.children) {
for (CZoneProp childProp : prop.prop_content_props) {
dbChild = takeMatch(childEntry, dbChildCandidates);
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() + "'");
if (dbChild == null)
continue;
}
dbChildren.remove(dbChild);
compareBuilding(childKey, childProp, dbChild, jsonChild);
entriesByDbUID.put(dbChild.getObjectUUID(), childEntry);
if (Math.abs(dbChild.getStatAlt() - jsonChild.getStatAlt()) > POSITION_TOLERANCE)
addDifference("child local Y", childKey + " DB " + dbChild.getStatAlt() + " JSON " + jsonChild.getStatAlt());
if (!compare)
continue;
matchedEntries.add(childEntry);
compareBuilding(childEntry, dbChild);
if (Math.abs(dbChild.getStatAlt() - childEntry.building.getStatAlt()) > POSITION_TOLERANCE)
addDifference("child local Y", childEntry.key + " DB " + dbChild.getStatAlt() + " JSON " + childEntry.building.getStatAlt());
// Today's runAfterLoad adds the child offset without the parent's yaw.
childMove = dbChild.getLoc().distance2D(jsonChild.getLoc());
childMove = dbChild.getLoc().distance2D(childEntry.building.getLoc());
if (childMove > POSITION_TOLERANCE) {
addDifference("child world x/z (parent yaw)", childKey + " moved " + childMove);
addDifference("child world x/z (parent yaw)", childEntry.key + " moved " + childMove);
maxChildMove = Math.max(maxChildMove, childMove);
}
}
// DB children of dropped twin children map to the kept child.
for (Building leftover : dbChildCandidates) {
twin = findTwin(leftover, entry.children);
if (twin != null)
entriesByDbUID.put(leftover.getObjectUUID(), twin);
}
}
// Same mesh and local x/z; removes and returns the closest DB building, or null.
private static Building takeMatch(Building jsonBuilding, ArrayList<Building> candidates) {
private static Building takeMatch(StaticBuildingEntry entry, ArrayList<Building> candidates) {
Building found = null;
float foundDistance = Float.MAX_VALUE;
@@ -170,10 +196,7 @@ public enum StaticBuildingParity {
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));
distance = localDistance(candidate, entry);
if (distance <= POSITION_TOLERANCE && distance < foundDistance) {
found = candidate;
@@ -181,17 +204,143 @@ public enum StaticBuildingParity {
}
}
if (found != null) {
if (found != null)
candidates.remove(found);
matchCount++;
}
return found;
}
private static void compareBuilding(String key, CZoneProp prop, Building dbBuilding, Building jsonBuilding) {
// An unmatched JSON prop with the same mesh in the DB row's zone or its parent zone: same name first,
// otherwise the only candidate.
private static StaticBuildingEntry findMovedMatch(Building dbBuilding, HashMap<Integer, ArrayList<StaticBuildingEntry>> entriesByZone) {
String prefix = key + " (DB " + dbBuilding.getObjectUUID() + ")";
ArrayList<StaticBuildingEntry> candidates = new ArrayList<>();
Zone zone = ZoneManager.getZoneByUUID(dbBuilding.parentZoneUUID);
addMovedCandidates(dbBuilding, entriesByZone.get(zone.getObjectUUID()), candidates);
if (zone.parent != null && zone.parent.isStaticWorldObject())
addMovedCandidates(dbBuilding, entriesByZone.get(zone.parent.getObjectUUID()), candidates);
for (StaticBuildingEntry candidate : candidates)
if (candidate.building.getName().equals(dbBuilding.getName()))
return candidate;
return candidates.size() == 1 ? candidates.get(0) : null;
}
private static void addMovedCandidates(Building dbBuilding, ArrayList<StaticBuildingEntry> zoneEntries, ArrayList<StaticBuildingEntry> candidates) {
if (zoneEntries == null)
return;
for (StaticBuildingEntry entry : zoneEntries)
if (!matchedEntries.contains(entry) && entry.building.meshUUID == dbBuilding.meshUUID)
candidates.add(entry);
}
// Any kept entry, matched or not, with the same mesh and local x/z.
private static StaticBuildingEntry findTwin(Building dbBuilding, ArrayList<StaticBuildingEntry> siblings) {
for (StaticBuildingEntry entry : siblings)
if (localDistance(dbBuilding, entry) <= POSITION_TOLERANCE)
return entry;
return null;
}
// Chebyshev distance on local x/z for the same mesh; MAX_VALUE for a different mesh.
private static float localDistance(Building dbBuilding, StaticBuildingEntry entry) {
if (dbBuilding.meshUUID != entry.building.meshUUID)
return Float.MAX_VALUE;
return Math.max(Math.abs(dbBuilding.statLat - entry.building.statLat), Math.abs(dbBuilding.statLon - entry.building.statLon));
}
private static void logResults(ArrayList<Building> dbStatic, ArrayList<StaticBuildingEntry> topLevelEntries) {
for (Building building : dbStatic)
if (!entriesByDbUID.containsKey(building.getObjectUUID()))
addDifference(DB_ONLY, building.getObjectUUID() + " zone " + building.parentZoneUUID + " mesh " + building.meshUUID
+ " '" + building.getName() + "' at " + building.statLat + ", " + building.statLon + " parent " + building.parentBuildingID);
for (StaticBuildingEntry entry : topLevelEntries) {
if (!matchedEntries.contains(entry))
addDifference("JSON only", entry.key + " mesh " + entry.building.meshUUID + " '" + entry.building.getName() + "'");
for (StaticBuildingEntry childEntry : entry.children)
if (!matchedEntries.contains(childEntry))
addDifference("JSON only", childEntry.key + " mesh " + childEntry.building.meshUUID + " '" + childEntry.building.getName() + "'");
}
Logger.info("Static building parity: " + propCount + " props, " + dbStatic.size() + " DB rows, " + entriesByDbUID.size()
+ " mapped (" + exactCount + " top-level exact, " + movedCount + " moved, " + twinCount + " twins, rest children)"
+ ", max child x/z move from the parent yaw " + maxChildMove);
// Every unmapped DB row is listed in full: the migration deletes it, so its references must be checked.
for (Map.Entry<String, ArrayList<String>> entry : differences.entrySet()) {
Logger.info(" " + entry.getKey() + ": " + entry.getValue().size());
for (int i = 0; i < entry.getValue().size(); i++)
if (i < EXAMPLES || entry.getKey().equals(DB_ONLY))
Logger.info(" " + entry.getValue().get(i));
}
}
// task24_static_map: every static zone (tree UID -> static ID) and every DB static building (UID -> static ID,
// or NULL when unmapped). sql/task24_static_switch.sql reads it.
private static void writeMigration(int mapID, ArrayList<Building> dbStatic) {
File migrationFile = new File(mapPath + mapID + ".migration.sql");
ArrayList<String> rows = new ArrayList<>();
StringBuilder sql = new StringBuilder();
StaticBuildingEntry entry;
for (Map.Entry<Integer, Integer> zoneID : new TreeMap<>(ZoneManager.staticIDsByTreeUID).entrySet())
rows.add("(" + zoneID.getKey() + ", " + zoneID.getValue() + ", 'zone', 'zone:" + zoneID.getKey() + "')");
for (Building building : dbStatic) {
entry = entriesByDbUID.get(building.getObjectUUID());
if (entry == null)
rows.add("(" + building.getObjectUUID() + ", NULL, 'building', NULL)");
else
rows.add("(" + building.getObjectUUID() + ", " + entry.staticID + ", 'building', '" + entry.key + "')");
}
sql.append("-- World Boot Plan 2.4: old UID -> static ID for map ").append(mapID).append(", written by MB_STATIC_BUILDINGS=parity.\n");
sql.append("DROP TABLE IF EXISTS `task24_static_map`;\n");
sql.append("CREATE TABLE `task24_static_map` (`oldUID` BIGINT UNSIGNED NOT NULL, `newID` INT NULL, ")
.append("`kind` ENUM('zone','building') NOT NULL, `staticKey` VARCHAR(64) NULL, PRIMARY KEY (`oldUID`));\n");
for (int i = 0; i < rows.size(); i++) {
if (i % INSERT_BATCH == 0)
sql.append("INSERT INTO `task24_static_map` VALUES\n");
sql.append(rows.get(i));
sql.append(i % INSERT_BATCH == INSERT_BATCH - 1 || i == rows.size() - 1 ? ";\n" : ",\n");
}
try {
Files.write(migrationFile.toPath(), sql.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
Logger.error("Static migration mapping not written: " + migrationFile.getAbsolutePath() + ": " + e);
return;
}
Logger.info("Static migration mapping: " + rows.size() + " rows written to " + migrationFile.getName());
}
private static void compareBuilding(StaticBuildingEntry entry, Building dbBuilding) {
Building jsonBuilding = entry.building;
CZoneProp prop = entry.prop;
String prefix = entry.key + " (DB " + dbBuilding.getObjectUUID() + ")";
String dbGuild = guildName(dbBuilding);
String jsonGuild = "";
@@ -219,7 +368,9 @@ public enum StaticBuildingParity {
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 (dbBuilding.getOwnerUUID() != jsonBuilding.getOwnerUUID())
addDifference("owner NPC", prefix + " DB " + dbBuilding.getOwnerUUID() + " JSON " + jsonBuilding.getOwnerUUID());
if (prop.prop_type == ZonePropType.CITY && GuildManager._npcGuildsByKey.containsKey(prop.city_prop_gm_guild))
jsonGuild = GuildManager._npcGuildsByKey.get(prop.city_prop_gm_guild).getName();
+19 -4
View File
@@ -40,6 +40,8 @@ public enum ZoneManager {
public static HashMap<Integer, CZoneTemplate> _zone_templates = new HashMap<>();
public static ArrayList<ZoneTreeEntry> zoneTree;
public static final HashMap<Integer, Zone> staticZonesByTreeUID = new HashMap<>();
public static final HashMap<Integer, Integer> staticIDsByTreeUID = new HashMap<>();
public static final Set<Zone> macroZones = Collections.newSetFromMap(new ConcurrentHashMap<>());
private static final ConcurrentHashMap<Integer, Zone> zonesByID = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD);
@@ -96,24 +98,37 @@ public enum ZoneManager {
return templateIDs;
}
// Static zones from the map's zone tree, then player-city zones from the DB. False aborts boot.
// Static zones from the map's zone tree, then player-city zones from the DB. Every static zone takes its
// static ID in tree order (W10); zones use it as their ID only once static buildings load from JSON (2.4).
// False aborts boot.
public static boolean loadZones() {
public static boolean loadZones(boolean useStaticIDs) {
ArrayList<Zone> cityZones;
Zone zone;
int staticID;
int parentZoneID;
int staticCount = 0;
for (ZoneTreeEntry treeEntry : zoneTree) {
if (treeEntry.parent != 0 && getZoneByUUID(treeEntry.parent) == null) {
if (treeEntry.parent != 0 && !staticZonesByTreeUID.containsKey(treeEntry.parent)) {
Logger.error("Zone " + treeEntry.uid + " (" + treeEntry.name + ") has no parent zone " + treeEntry.parent);
return false;
}
zone = Zone.createStaticZone(treeEntry);
staticID = DbManager.nextStaticID(mbEnums.GameObjectType.Zone, "zone:" + treeEntry.uid);
staticIDsByTreeUID.put(treeEntry.uid, staticID);
if (treeEntry.parent == 0)
parentZoneID = 0;
else
parentZoneID = staticZonesByTreeUID.get(treeEntry.parent).getObjectUUID();
zone = Zone.createStaticZone(treeEntry, useStaticIDs ? staticID : treeEntry.uid, parentZoneID);
DbManager.addToCache(zone);
zone.runAfterLoad();
staticZonesByTreeUID.put(treeEntry.uid, zone);
staticCount++;
}
+17
View File
@@ -3178,6 +3178,23 @@ public class mbEnums {
TRAINER
}
// MB_STATIC_BUILDINGS (World Boot Plan 2.4). DB: static buildings from obj_building, before the migration.
// PARITY: DB load plus the JSON buildings compared and the migration mapping written. JSON: after the migration.
public enum StaticBuildingMode {
DB,
PARITY,
JSON;
public static StaticBuildingMode fromConfig(String configValue) {
for (StaticBuildingMode mode : values())
if (mode.name().equalsIgnoreCase(configValue.trim()))
return mode;
return DB;
}
}
public enum PointSetType {
POINTS_ORDEREDREV
}
+13 -7
View File
@@ -220,15 +220,16 @@ 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) {
// A static building from a CZONE prop (W4). 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). ID 0 keeps the building out of the world grid.
public static Building createStaticBuilding(int buildingID, Zone parentZone, CZoneProp prop, Building parentBuilding, int ownerUUID) {
Building building = new Building();
CZonePlacement placement = prop.prop_basic_zone;
Vector3fImmutable localLoc = placement.basic_zone_spawn_location;
building.setObjectUUID(buildingID);
building.setStaticWorldObject(true);
building.setObjectTypeMask(MBServerStatics.MASK_BUILDING);
building.gridObjectType = GridObjectType.STATIC;
@@ -245,6 +246,8 @@ public class Building extends AbstractWorldObject {
building.protectionState = ProtectionState.NPC;
building.maxGold = 15000000;
building.ownerUUID = ownerUUID;
building.ownerIsNPC = ownerUUID != 0;
if (prop.prop_type == ZonePropType.CITY) {
building.rank = prop.city_prop_asset_rank;
@@ -1062,7 +1065,9 @@ public class Building extends AbstractWorldObject {
resists = new Resists("Building");
if (this.parentZone != null) {
// Static buildings got their world location and NPC owner from createStaticBuilding.
if (this.parentZone != null && !this.isStaticWorldObject()) {
if (this.parentBuildingID != 0) {
Building parentBuilding = BuildingManager.getBuilding(this.parentBuildingID);
if (parentBuilding != null) {
@@ -1083,8 +1088,9 @@ public class Building extends AbstractWorldObject {
// City load reads ownerIsNPC before NPCs are cached, so boot uses the owner set read ahead of buildings.
this.ownerIsNPC = BuildingManager._npcBuildingOwners.contains(this.ownerUUID)
|| DbManager.inCache(GameObjectType.NPC, this.ownerUUID);
if (!this.isStaticWorldObject())
this.ownerIsNPC = BuildingManager._npcBuildingOwners.contains(this.ownerUUID)
|| DbManager.inCache(GameObjectType.NPC, this.ownerUUID);
// Reference friend and condemn lists from BuildingManager
+4 -4
View File
@@ -110,14 +110,14 @@ public class Zone extends AbstractWorldObject {
}
// Static zones keep their positive tree UIDs until the W10 switch to negative IDs
public static Zone createStaticZone(ZoneTreeEntry treeEntry) {
// zoneID is the static ID, or the tree UID while static buildings still load from the DB (W10)
public static Zone createStaticZone(ZoneTreeEntry treeEntry, int zoneID, int parentZoneID) {
Zone zone = new Zone();
zone.setObjectUUID(treeEntry.uid);
zone.setObjectUUID(zoneID);
zone.setStaticWorldObject(true);
zone.parentZoneID = treeEntry.parent;
zone.parentZoneID = parentZoneID;
zone.templateID = treeEntry.template;
zone.zoneName = treeEntry.name;
zone.peace_zone = treeEntry.peaceZone ? (byte) 1 : (byte) 0;
+23 -3
View File
@@ -286,6 +286,9 @@ public class WorldServer {
private boolean init() {
mbEnums.StaticBuildingMode staticBuildingMode;
int staticZoneRows;
Logger.info("MAGICBANE SERVER GREETING:");
Logger.info(ConfigManager.MB_WORLD_GREETING.getValue());
@@ -548,6 +551,18 @@ public class WorldServer {
long start = System.currentTimeMillis();
staticBuildingMode = StaticBuildingLoader.getMode();
staticZoneRows = DbManager.StaticWorldQueries.COUNT_STATIC_ZONE_ROWS();
Logger.info("Static buildings: MB_STATIC_BUILDINGS " + staticBuildingMode + ", " + staticZoneRows + " static zone rows in obj_zone");
// The 2.4 migration deletes the static rows: JSON needs it run, DB and PARITY need it not run yet.
if (staticZoneRows < 0 || (staticBuildingMode == mbEnums.StaticBuildingMode.JSON) != (staticZoneRows == 0)) {
Logger.error("ABORT! MB_STATIC_BUILDINGS " + staticBuildingMode + " does not fit this DB: " + staticZoneRows
+ " static zone rows. Use JSON after the World Boot Plan 2.4 migration, DB or PARITY before it.");
exitServer(1);
}
Logger.info("Loading NPC guilds");
if (!GuildManager.loadStaticGuilds(worldMapID)) {
@@ -557,13 +572,18 @@ public class WorldServer {
Logger.info("Loading all Zones");
if (!ZoneManager.loadZones()) {
if (!ZoneManager.loadZones(staticBuildingMode == mbEnums.StaticBuildingMode.JSON)) {
Logger.error("ABORT! Zones failed to load!");
exitServer(1);
}
GuildManager.logMissingGuildKeys();
if (staticBuildingMode == mbEnums.StaticBuildingMode.JSON) {
Logger.info("Loading static Buildings from the zone JSON");
StaticBuildingLoader.buildStaticBuildings(true);
}
Logger.info("Loading all Buildings");
BuildingManager._npcBuildingOwners = DbManager.BuildingQueries.GET_NPC_BUILDING_OWNERS();
DbManager.BuildingQueries.GET_ALL_BUILDINGS();
@@ -578,9 +598,9 @@ public class WorldServer {
Logger.info("Loading all Mobiles");
DbManager.MobQueries.GET_ALL_MOBS();
if (StaticBuildingParity.isEnabled()) {
if (staticBuildingMode == mbEnums.StaticBuildingMode.PARITY) {
Logger.info("Comparing static buildings with the zone JSON");
StaticBuildingParity.run();
StaticBuildingParity.run(worldMapID);
}
StaticManifest.writeManifest(worldMapID);