A missing setting threw a NullPointerException, and an unknown one fell
back to DB without a word. Both now abort with a message, as
MB_STATIC_RUNEGATES does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runegate source is now an explicit StaticRunegateMode, like
MB_STATIC_BUILDINGS. Boot aborts when the setting is missing or unknown,
or when it is JSON while static buildings are not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
World Boot Plan 2.7. In JSON building mode a runegate is a static building
whose CZONE prop has a registry name, and its portals come from
maps/<map>.gates, keyed by that name. A gate on the map with no file entry,
a file key or target that is not on the map, a duplicate registry name or a
missing portal type aborts boot. MB_STATIC_RUNEGATES=db keeps the
static_runegate_portals load; the file load logs its differences with that
table.
CITYDATA runegate entries now send the building's name ("Runegate" when the
prop has none) instead of the zone's, in ascending object ID order, as live
servers did.
Value changes on map 108 (2.6 decisions):
- Gate of Vollistor is no longer a runegate (8 portals).
- Graven Rock EARTH, Dovaal's Gate AIR, Ring of Kauro FIRE and Sigil Stone
WATER lead to the Forbidden Gate instead of back to themselves.
- CITYDATA gate names: Khar Th'Sekt "Runegate", Maelstrom's Eye
"Chaos Runegate", The Forbidden Gate "Forbidden Gate", The Doomgate
"Doomgate", The Ring of Kauro "Ring of Kauro".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbEnums.Ruins held the old obj_zone UIDs 569-576; since 2.4 the Ruins are
tree UIDs 643-650 with static IDs, so MB_USE_RUINS=true would hit a null
zone. 108.guild now lists the 8 Ruins tree keys under ruinKeys (moved out
of noGuildKeys; still "no guild"), and boot collects the static zones
whose CITY props carry one: exactly the 8 Ruins on map 108. A zone-name
match was ruled out: "ruin" names 57 zones, mostly mob camps.
The enum is replaced by ZoneManager.getRandomRuinLocation(). It falls back
to Sea Dog's Rest when ruins are off (unchanged) or none were found.
Value changes: none with MB_USE_RUINS off. With it on, unbound respawns go
to a random Ruin again instead of failing on a missing zone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
World Boot Plan 2.5. One NPC city per seed guild, in the zone of the
guild's first CITY prop (tree order, then JSON order); that prop is the
tree of life. City IDs follow all static buildings (key city:<zone>/<prop>),
so no existing static ID moves. Fields the JSON lacks take the values all
24 DB NPC city rows had; the hash stays in memory.
With MB_STATIC_BUILDINGS=json the DB decides: while obj_city still holds
NPC cities they load from the DB, the JSON cities only take their IDs, a
parity report is logged and maps/<map>.city_migration.sql is written for
sql/task25_static_cities.sql. Once those rows are gone the cities load
from the JSON. The city's guild still comes from its tree's owner (the
DB NPC guild); the static guild's owned city is set in memory.
Value changes once switched:
- City IDs: DB UIDs -> static IDs (obj_guild.ownedCity re-pointed).
- Khar Th'Sekt's tree: the runegate (prop 0, mesh 24500) -> the Tree of
Life (prop 1, mesh 24000). No character binds point at the runegate.
- forceRename true -> false on all 24 (only read for a player guild's tree).
- The orphan check also covers obj_guild.ownedCity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NPC city noob-isle flag (wire and teleport/repledge rules) has no
source in the client data. On map 108 it is set for the 18 cities under
Tyrranth Minor; 108.guild now carries it per seed guild, next to the
repledge and teleport limits. Missing means false.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live client also draws the Citadel of War's upper-floor table on the
ground floor (ten-box recording), so the ID order was not the cause. The
order stays: parents below their children, as live IDs and DB UIDs were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live LOADSTRUCTURE records send static props and their furniture with
objectid_d and objectid_e zero and unknown_bool_b false; furniture also
has 0/0 health (4,506 of 4,506 live child props). The server sent its own
ID, the owner, the flag set and health for every building. Static
buildings now write the live values; player buildings keep the old block.
Still different from live, not touched here: top-level static health (live
0 for most props), the int_c icon on some props, and the object type word.
Value changes (wire only): static buildings send no own ID, owner or flag;
static furniture sends 0/0 health.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upper-floor furniture of static buildings showed on the ground floor
(Citadel of War, Volgarth Holm) although the server sends its CZONE local
Y (27, as live). Recordings store each cell's objects in ascending ID
order, and live child props always had higher IDs than their parent; our
counter gave every child a lower ID than its parent. GUESS (not traced in
the client): the client places a cell's objects in ID order and a child
seen before its parent loses its height. Children now take their IDs, in
JSON order, before their parent, so the parent has the lowest ID of its
family. Zone IDs and the prop order are unchanged.
This renumbers the static buildings. When the manifest diff finds moved
IDs, boot writes maps/<map>.renumber.sql (old -> new per key) and stops;
boot also refuses to start while that file exists, so the DB is never
played with references on the wrong objects. World-Boot-Plan
sql/static_renumber.sql applies it. Moved and re-targeted lines in the
log are capped at 20 each.
Value changes: every static building ID moves (DB references re-pointed
by the renumber SQL); no field values change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit ca29031d. Its evidence was an artifact: recordings
store each LOADSTRUCTURE's objects in ascending object ID order (782 of 782
live records, and our citadel tests whatever order the server wrote), so
they do not show the send order. The furniture was still on the ground
floor with the change in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The structure list comes from a HashSet, so its order was never fixed:
positive UIDs happened to put a parent before its children, negative
static IDs put the children first. Live recordings send the parent first
in all 2,365 parent/child pairs within one record (none the other way);
the citadel-test recording had all 616 children first, and the Citadel of
War's upper-floor table and chairs (CZONE Y 27, sent correctly) showed on
the ground floor. Top-level buildings are now written first, then children.
Value changes: record order only; every field is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
MOVETOPOINT's building field is an objectid with 0 for the open world
(docs/messages.md, verified). The > 0 test skipped the building-local to
world conversion for buildings with negative static IDs (World Boot Plan
2.4); != 0 keeps every positive ID as before.
Value changes: none for positive building IDs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hasher.encode indexes arrays with the number, so a negative static zone
ID (World Boot Plan 2.4) throws. encryptObjectID hashes a negative ID as
the pair (0, -id), which no single positive ID can produce; positive IDs
hash as before. Static zones and PvP records use it; mine records read
the zone's stored hash.
Value changes: none until static zones take negative IDs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The object.parent foreign key (ref_parent, ON DELETE CASCADE) is dropped
in World Boot Plan 2.4: static zones and buildings leave the DB, so rows
under them get negative parents that no object row holds. deleteObjectTree
deletes an object and every row below it by parent in one transaction,
which is what the cascade did. Used by the building, guild, mob, NPC and
zone delete paths (city destroy relies on it for the city, NPCs and mobs).
Value changes: none; the same rows are deleted, and the FK is still in
place until the 2.4 migration runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
nextStaticID now takes the object type and a natural manifest key, so no
static ID is handed out without a manifest entry. After the world load the
server diffs maps/<map>.manifest against the previous boot (moved, vanished,
re-targeted IDs), writes the new one (old copy kept as .manifest.prev), and
logs every negative reference in the Task 2.1 columns that no loaded static
object of the right type holds.
Value changes: none in play. Guilds are the only static IDs so far
(-2..-25, keys guild:<first seed key>).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The world server read all 861 CZONE files; map 108 uses 633. The zone tree
is now read first and the parser loads only the templates it names. A
template with no CZONE file aborts boot from the parser, replacing the
check in loadZones.
The tree no longer carries player-city zones (mbScryer drops them at
export), so isPlayerCity and its skip are gone. An old tree still holding
them aborts boot: template 0 has no CZONE file.
Value changes: none; 228 unused templates are no longer in memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ItemProductionMsgHandler trusted the client to send production, recycle,
junk, deposit, price and take requests only from an open management
window. It now applies the window's own rule (owner, guild leader or IC,
or building friends) before any action.
Value changes: none for players who can open the window; others are
silently ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forge submit and vendor recycle took gold from, or refunded gold to, the
vendor building's strongbox with no protection check. NPC-city vendors
could roll on the static building's 2000 gold. Vendor buy, repair and train
already skip NPC buildings.
Value changes: forging at a vendor in a protection-NPC building now fails
with popup 58; recycling there does nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GuildManager.loadStaticGuilds builds the map's NPC guilds from
maps/<MB_WORLD_MAPID>.guild through Guild.createStaticGuild, before zones,
so they take the first static IDs (-2 down, file order). Each is flagged
static, is its own nation (as live and in the DB) and is indexed by its
CZONE guild and nation keys. Boot aborts on a missing or bad seed file or a
duplicate key. After zones load, every CITY-prop guild key with no seed
entry is logged once with its zones; known no-guild keys are only counted.
Nothing references the static guilds yet: NPC cities, NPCs and players
still use the DB guilds (UID 2-25) until 2.5, so there is no gameplay change.
Seed vs the 24 DB NPC guilds (scripts/task16_guild_seed.py): name, crest,
leadership type, motto and repledge/teleport limits identical. Charter
differs on all 24: DB 562 (BARBARIAN placeholder) -> guilddata value, read
as the client guild type index (sb.exe g_GuildTypeNames 0x175B8E0), e.g.
Kingscross NOBLE, Bastion CATHEDRAL, Khar Th'Sekt VIRAKT, hamlets MILITARY.
Static guilds have no warehouse hash yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GuildSeedParser reads the server-owned NPC guild seed (W7): guilds in
static ID order, each with its CZONE city_prop_gm_guild keys, nation key,
name, crest, charter (GuildCharterType name), leadership type, motto and
repledge/teleport limits, plus the known no-guild keys. An unknown charter,
a guild with no keys or a crest without five values fails the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tinylog's writing thread dropped queued lines on System.exit, so boot
aborts died with no error in the log. exitServer shuts the writing thread
down (flushing it) before exiting; every exit in WorldServer uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZoneManager.loadZones builds the 1,062 static zones from
maps/<MB_WORLD_MAPID>.tree through Zone.createStaticZone, flagged static
and keeping their positive tree UIDs (negative IDs come with W10 in 2.4).
Player-city zones still load from obj_zone (template 0). Boot aborts on a
missing tree file, a missing CZONE template or a missing parent.
GET_ALL_ZONES is kept until the removal task.
Value changes vs obj_zone (parity: scripts/task14_zone_parity.py):
- 44 zones: x/z offsets take the live values; obj_zone had them rounded
to whole units (e.g. 279: 192.0 -> 192.48, 274: -238.0 -> -237.653).
- Static zones no longer write their hash to obj_zone at boot; the hash
is computed in memory (same value).
- min/max level 30/60 for the 50 icon zones now comes from constants
(no client data carries zone levels); same values as obj_zone.
Everything else identical: parent (except the unused root parent),
template, name, peace flag, y offset, icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reads uid, parent, template, name, isPlayerCity, offset, peaceZone and
the three icons. Skips childCount (derived), objectId (live NEWWORLD ID,
not needed yet) and extents (player-city only). A non-zero yaw, a missing
offset or a map ID that differs from MB_WORLD_MAPID fails the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZoneManager._zone_templates now holds the parser's CZoneTemplate: every
CZONE template plus the player-city template 0, still built in code.
Zone and Terrain read the mbEnums sea level and terrain types instead of
strings. The static_zone_templates loader goes with the old map type;
the table stays until Task 4.2.
Parity: all 861 templates, every column the server read (type, name,
peace, radii, blends, sea level and type, water, terrain gen, type,
max_y, image) identical to the JSON. Value changes: none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZoneParser reads mb.data/CZONE/<id>.json into CZoneTemplate with its
props, child props, mobiles and point sets (World-Boot-Plan Task 1.1).
Only the agreed server-needed fields are parsed; textures, water,
weather, sound and always-default fields are skipped. Boot aborts if
any file fails. Nothing uses the data yet (Task 1.2).
New mbEnums: ZoneShapeType, SeaLevelType, TerrainGenType, ZonePropType,
ZoneMobileType, PointSetType. root FSM maps to MobBehaviourType and
monster types to MonsterType.
Value changes: none (load only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
World Boot Task 0.6. Static objects are built from client data each boot,
so dev commands answer "Must use external tooling to modify gameworld" and
log the command, target and caller instead of changing them.
- /addbuilding, /mob and /npc always refuse (addsWorldContent hook in doCmd).
- rotate, pull, setbuildingaltitude, remove, rename, npcname, setowner,
setguild, setrank and forcerename refuse a static target.
- purge skips static objects and reports how many it skipped.
Value changes: none for non-static objects; the purge message now also
reports the skipped count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DbManager.refuseStaticWrite logs the handler method and its caller and
stops the write (W3). It guards every write whose row belongs to a
Building, Zone, City, Guild, Mob or NPC: 69 handler methods, including
friends/condemned/patrol lists, guard minions, NPC profits and guild
banish/alliance lists. ID-only methods look the object up in the cache.
Writes that only reference a static object (character guild history,
another guild's alliance row) are not guarded.
Value changes: none; nothing is flagged static yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Static world objects take IDs from DbManager.nextStaticID(), counting down
from -2 in a fixed load order (W2). Runtime negative IDs (loot, forged and
contract items, pets and minions) now share ItemManager.lastNegativeID,
which starts at -1,000,000; Mob.staticID is gone. Boot fails if the static
counter reaches the runtime range. AbstractWorldObject gets a
staticWorldObject flag for the factories to set. Nothing uses either yet.
Value changes: runtime negative IDs start at -1,000,001 (-1,000,000 for
forged items) instead of -1; pets and minions draw from the item counter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The boot run was clean: 187,824 building quaternion parts and 11,738 mob
yaws matched exactly. The two NPC reports (14876, 16700) were a flaw in the
replay, not the switch: their contract 2100 does not exist, so
NPC.runAfterLoad returns before slotting, and old and new both send pi/2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/rotate took degrees but stored them on NPCs and mobs where the code expects
cos of half the yaw, so the client got NaN and the DB got degrees; /rotate
face passed radians the same way. Buildings used |cos| for w, which mirrored
any angle above 180 (270 became 90). Every kind now takes a yaw in radians
(degrees from the command, the facing angle for "face") through
Quaternion.fromYaw. NPC and mob yaws are wrapped to [0, 2 pi) so the stored
cos reloads to the same angle.
Value changes: /rotate N on NPCs/mobs now faces N degrees (was NaN);
buildings past 180 degrees now face the given angle; npc_rotation and
mob_rotation saved by /rotate hold cos of half the yaw instead of the raw
input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under MB_PARITY_CHECK, after the mobs load, replays the old rotation code
(building w/rotY, npc_rotation as cos of half the yaw, mob default 0, the old
slot math and serializer) on the DB values and compares it with what the
loaded objects now send. Yaws are compared modulo 2 pi within 1e-5. Dev-only;
to be deleted once a run is clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbstractWorldObject.rot (a Vector3f holding one quaternion part) becomes a
Quaternion. Building drops its separate w field; Mob and NPC serialize
through AbstractCharacter.getClientYaw. DB columns are unchanged and are
converted at load and save: building (w, rotY) map to the quaternion's w and
y, npc_rotation (cos of half the yaw) becomes fromYaw(2 acos(value)).
Wire values are meant to be unchanged; a parity run follows. Behaviour kept
on purpose:
- mob_rotation is still not loaded, so every mob defaults to yaw pi as before
- the slot rotation still reads the product's sin as a cos
- /rotate on NPCs and mobs still treats its input as cos of half the yaw
Value changes: building serializer writes the quaternion's x and z instead
of literal zeros (both are 0 for every building); /info "Rot" for NPCs and
mobs prints the quaternion y (sin of half the yaw) instead of the stored cos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transform holds location, rotation and scale as the client's ArcObjectInfo
does, with parent x local composition (sb.exe 0x6516C0). Quaternion.fromYaw
matches the client's MakeRotateY and getYaw returns the angle it would need.
Nothing uses them yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
boolToken follows Core.dll String::ToBool with a false default instead of
throwing, and the five PowerActions keys the client compares to exact
"TRUE" use the new optionalExactTrueToken.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Building.runAfterLoad ran GET_UID_ENUM for every building (~47k queries).
One query now reads the NPC owner IDs before the building load; runtime
loads also check the NPC cache. City load reads ownerIsNPC before NPCs
are cached, so a pass after the NPC load would not work.
Value changes: none. ownerIsNPC is still true exactly when the owner's
object.type is 'npc' (dev DB: 388 buildings, 24 owners).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Building gets a no-arg constructor, a createBuilding factory (in the style
of Mob.createMob) and persistBuilding, which inserts the row through
building_CREATE, sets the returned UID, caches the object and runs
runAfterLoad. The GET_BUILDINGBYUUID re-select after the insert is gone.
PlaceAssetMsgHandler (createStructure, createBarracks, createWarehouse),
Bane and AddBuildingCmd move onto it.
Value changes:
- floor/level start at -1 and the other unset fields at the obj_building
column defaults, same as the reloaded row.
- upgradeDateTime keeps milliseconds instead of the DATETIME-rounded value
(sub-second difference in the upgrade job time).
- Warehouse placement no longer runs runAfterLoad twice, which submitted
the warehouse upgrade job twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PowerReq lists now come from the rune JSON on world and login: level from
power_value, skill and power prereqs from power_granted_skills/powers.
The DB table is no longer read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>