Commit Graph
3479 Commits
Author SHA1 Message Date
MagicBotandClaude Opus 5.5 08b50eecf0 Load static trainers from the zone JSON, comparing with the DB until migrated
World Boot Plan 3.2. With MB_STATIC_BUILDINGS=JSON, every TRAINER zone
mobile takes a static ID after the cities (npc:<zone>/m<i>,
npc:<zone>/<prop>/m<i>). While obj_npc still holds trainers of the same
zone and name, the boot only compares and writes
maps/<map>.trainer_migration.sql; afterwards the JSON trainers load
before the DB vendors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:54:14 -04:00
MagicBotandClaude Opus 5.5 6a3480a0dd Build static NPCs from zone mobiles with their CZONE runes and equipment
NPC.createStaticNPC fills a TRAINER zone mobile; static NPCs send the CZONE
rune list ordered by kind (RuneType.mobileRuneKind) and wear the
mobile_equipment list. DB NPCs keep the contract rune rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:54:14 -04:00
MagicBotandClaude Opus 5.5 2fcdcf9f0d Load the NPC guild seed on the login server
Character select sends each character's guild, and since 2.8 the NPC
guilds exist only in maps/<map>.guild. The login server loaded guilds
from the DB only, so characters in NPC guilds showed as errant. It now
loads the seed guilds first, as the world server does; a seed that fails
to load fails the login server boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:31:41 -04:00
MagicBotandClaude Opus 5.5 22acab6d79 Hash negative guild IDs in warehouse records; check friend lists for orphans
Players in an NPC guild now carry a negative guild ID, and Hasher.encrypt
throws on negative numbers. PvP and character history records hash guild
and nation IDs with encryptObjectID, as static zones already do (2.4);
positive IDs hash as before.

The boot orphan check also covers dyn_building_friends.guildUID and
dyn_building_condemned.guildUID, which 2.8 makes signed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 07:57:24 -04:00
MagicBotandClaude Opus 5.5 b10dd4cc6e Switch NPC cities, CITY props and NPCs to the static seed guilds
World Boot Plan 2.8. The 24 static seed guilds (-2..-25) become the only
NPC guilds. Static top-level CITY props have no owner and take their
guild from the seed key (city_prop_gm_guild), so Building.getGuild and
City.getGuild no longer go through the DB leader NPCs (30-54), which the
migration deletes with DB guilds 2-25. Static guilds count as NPC guilds
(repledge full member, swear to an NPC nation, bane rule, guild info).

Boot check: a DB guild named like a seed guild means the migration hasn't
run. The boot writes maps/<map>.guild_migration.sql (static ID -> old UID
by name) for sql/task28_npc_guild_switch.sql and aborts.

Value changes:
- Guild IDs on the wire: guild, nation and ruler IDs in CITYDATA, guild
  IDs of characters, NPCs and mobs in NPC guilds: 2..25 -> -2..-25.
- Static CITY prop owner: leader NPC -> none (live sent none).
- Leader name source: the seed's leaderName in CITYDATA, guild info and
  the tree status window (live Sea Dog's Rest: "Captain Quint"); names are
  identical to the deleted leader NPCs.
- Asset info window on an NPC-city prop: owner name -> blank (live
  "Statue of Braialla" sent a blank owner and the NPC guild name).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 07:57:24 -04:00
MagicBotandClaude Opus 5.5 173b97ffff Pick the realm map from the zone tree's world
mbEnums.WorldMap maps the world name to its realm map: Aerynth 3000000,
Dalgoth 3000001, Vorringia 3000002 (live REALMDATA sent 3000000 on
Aerynth and 3000002 on Vorringia; Dalgoth confirmed from the image).
The world comes from the "world" field mbScryer writes into
maps/<map>.tree, so it is user-editable; an unknown world aborts boot.

MB_WORLD_REALMMAP is removed: REALMDATA and realmmaps/<id>.png both take
the realm map from the world.

Value changes: none while MB_WORLD_REALMMAP matched the map's world.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 07:01:13 -04:00
MagicBotandClaude Opus 5.5 28b374c5c9 Send the asset ID for static CITY props
Double-clicking an NPC city tree gave "Invalid asset identifier": the
client opens an asset only when its LOADSTRUCTURE asset ID is set
(sb.exe 0xBD14E0), and d160c367 zeroed it on every static building.

Outside the first LOADSTRUCTURE of a recording, live sent the asset ID
with the flag set for top-level CITY props (198 of 199 templates) and
never for other props (866 templates); NPC trees had no owner. Static
top-level CITY props now send their own ID, no owner and the flag set.

Value changes (wire only): static top-level CITY props send their ID
and the flag again; the owner stays zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:43:05 -04:00
MagicBotandClaude Opus 5.5 6fdd5f425a Send the seed leader name for static NPC guilds in city data
Static guilds have no leader character, so the city and nation leader
names come from the guild seed's leaderName, as live CITYDATA sends them.
Other guilds keep the leader's name; a nation with no cached leader now
sends an empty name instead of throwing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:18:46 -04:00
MagicBotandClaude Opus 5.5 86d8859f54 Read the NPC guild leader name from the guild seed
leaderName comes from the old server's guildleaders.txt, joined to
guilddata by ID; it matches CITYDATA leader_name for every guild seen live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:17:29 -04:00
MagicBotandClaude Opus 5.5 0bbea064a8 Abort boot when MB_STATIC_BUILDINGS is missing or unknown
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>
2026-10-28 06:03:15 -04:00
MagicBotandClaude Opus 5.5 f190af70ad Require MB_STATIC_RUNEGATES to be DB or JSON
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>
2026-10-28 05:55:15 -04:00
MagicBotandClaude Opus 5.5 f8b51b515a Load runegates from the registry props and the map's gates file
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>
2026-10-28 05:50:56 -04:00
MagicBotandClaude Opus 5.5 0c38ae6844 Find the Ruins respawn zones from the seed's ruin keys
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>
2026-10-28 05:15:29 -04:00
MagicBotandClaude Opus 5.5 143b6a9d01 Build NPC cities from the CITY props and the guild seed
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>
2026-10-28 05:12:48 -04:00
MagicBotandClaude Opus 5.5 e3142b1bd8 Read an optional noobIsle flag from the guild seed
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>
2026-10-28 05:12:47 -04:00
MagicBotandClaude Opus 5.5 4154b07064 Drop the disproved furniture guess from the static ID order comment
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>
2026-10-28 01:26:13 -04:00
MagicBotandClaude Opus 5.5 d160c36709 Serialize static buildings' asset block as live servers did
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>
2026-10-28 01:25:45 -04:00
MagicBotandClaude Opus 5.5 7b5a857802 Give a static prop's children their IDs before the prop itself
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>
2026-10-28 01:08:55 -04:00
MagicBotandClaude Opus 5.5 8ac0d9c27a Revert "Send parent buildings before their furniture in LOADSTRUCTURE"
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>
2026-10-28 01:06:11 -04:00
MagicBotandClaude Opus 5.5 ca29031d7f Send parent buildings before their furniture in LOADSTRUCTURE
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>
2026-10-28 00:24:26 -04:00
MagicBotandClaude Opus 5.5 4c2648046b 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>
2026-10-27 23:37:54 -04:00
MagicBotandClaude Opus 5.5 5a89a14447 Treat any non-zero move building ID as inside a building
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>
2026-10-27 23:37:38 -04:00
MagicBotandClaude Opus 5.5 2f64fd43a3 Hash negative zone IDs for warehouse records without failing
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>
2026-10-27 23:31:24 -04:00
MagicBotandClaude Opus 5.5 3fbf07c2cb Delete object descendants in Java instead of through the parent FK cascade
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>
2026-10-27 23:31:02 -04:00
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
MagicBotandClaude Opus 5.5 b83cde8141 Write the static ID manifest and check DB references at boot
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>
2026-10-27 22:27:50 -04:00
MagicBotandClaude Opus 5.5 3b4a60c739 Parse only the zone templates the map's zone tree uses
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>
2026-10-27 22:11:37 -04:00
MagicBotandClaude Opus 5.5 807a806a0d Check building access on every vendor production action
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>
2026-10-27 21:50:31 -04:00
MagicBotandClaude Opus 5.5 4b50a4f74b Skip the strongbox of NPC buildings when forging and recycling
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>
2026-10-27 21:38:05 -04:00
MagicBotandClaude Opus 5.5 afb911cb72 Reword the static guild hash comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:16:06 -04:00
MagicBotandClaude Opus 5.5 42a6070d76 Load static NPC guilds from the guild seed at world server boot
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>
2026-10-27 21:15:09 -04:00
MagicBotandClaude Opus 5.5 4839883530 Parse the map guild seed file (maps/<map>.guild)
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>
2026-10-27 21:15:09 -04:00
MagicBotandClaude Opus 5.5 21975e0bed Flush the log before a world server boot abort
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>
2026-10-27 20:23:57 -04:00
MagicBotandClaude Opus 5.5 c66ce6e777 Build static zones from the zone tree at world server boot
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>
2026-10-27 20:07:21 -04:00
MagicBotandClaude Opus 5.5 f7b81f46a5 Parse the map zone tree file (maps/<map>.tree)
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>
2026-10-27 20:07:21 -04:00
MagicBotandClaude Opus 5.5 9419d02f42 Remove the unused ZoneTemplate class
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:28:58 -04:00
MagicBotandClaude Opus 5.5 4299b27422 Build zone templates from CZONE instead of static_zone_templates
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>
2026-10-27 19:28:58 -04:00
MagicBotandClaude Opus 5.5 ac1fcaecf5 Parse CZONE zone templates at world server boot
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>
2026-10-27 19:11:45 -04:00
MagicBotandClaude Opus 5.5 a8b8858f0e Revert the temporary /staticflag test command
Task 0.6 is tested; the command is no longer needed.

This reverts commit d295d044.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 19:03:01 -04:00
MagicBotandClaude Opus 5.5 d295d044bc TEMP: /staticflag toggles the static flag for testing Task 0.6
Revert after the 0.6 test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:58:02 -04:00
MagicBotandClaude Opus 5.5 bd3787f9d5 Refuse dev commands that would change static world objects
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>
2026-10-27 18:57:40 -04:00
MagicBotandClaude Opus 5.5 49ad45d179 Show "none" for the database ID of unsaved mobs and NPCs in /info
Pets, guard and siege minions have no DB row and printed
NO_DB_ROW_ASSIGNED_YET (2147483647).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:40:32 -04:00
MagicBotandClaude Opus 5.5 731425e85a Refuse DB writes for static world objects
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>
2026-10-27 07:45:31 -04:00
MagicBotandClaude Opus 5.5 c89bda35da Add the static world ID counter and static flag
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>
2026-10-27 07:43:17 -04:00
MagicBotandClaude Opus 5.5 740719aeb2 Remove the rotation parity check
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>
2026-10-27 06:50:32 -04:00
MagicBotandClaude Opus 5.5 bbf978872b Fix /rotate for NPCs, mobs and buildings past 180 degrees
/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>
2026-10-27 06:43:28 -04:00
MagicBotandClaude Opus 5.5 e288524ec0 Add a boot rotation parity check for the quaternion switch
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>
2026-10-27 06:42:31 -04:00
MagicBotandClaude Opus 5.5 bb76596a9a Store Building, Mob and NPC rotation as a client-order quaternion
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>
2026-10-27 06:40:59 -04:00
MagicBotandClaude Opus 5.5 2078a5c344 Add Transform and client yaw helpers to Quaternion
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>
2026-10-27 06:40:59 -04:00
MagicBotandClaude Opus 5.5 74b2eaaaf2 Read SetItemFlag, Track and TransferStat header flags as integers
The client parses these with String::ToInt(token) != 0, not ToBool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:41:30 -04:00