Compare commits

...
44 Commits
Author SHA1 Message Date
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
MagicBotandClaude Opus 5.5 c9474cf042 Read wpak booleans the way the client does
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>
2026-10-26 20:33:10 -04:00
MagicBotandClaude Opus 5.5 99e384cfe1 Point the WpakLine number comment at the client's strtol/strtod parsers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:31:27 -04:00
MagicBotandClaude Opus 5.5 03399bece7 Clear the NPC building owner set after the NPC load
After boot the NPC cache answers ownerIsNPC, so the set is not kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:20:03 -04:00
MagicBotandClaude Opus 5.5 35f1738ee4 Read NPC building owners once instead of per building at boot
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>
2026-10-26 19:16:53 -04:00
MagicBotandClaude Opus 5.5 015d536337 Remove CREATE_BUILDING
Every caller now goes through Building.createBuilding and persistBuilding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:02:17 -04:00
MagicBotandClaude Opus 5.5 d586536c74 Create player buildings in memory with a single DB write
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>
2026-10-26 16:02:10 -04:00
74 changed files with 3612 additions and 446 deletions

No files matched your search

+6 -5
View File
@@ -11,6 +11,7 @@ package engine.InterestManagement;
import engine.gameManager.ZoneManager;
import engine.math.Vector2f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.objects.Zone;
import org.pmw.tinylog.Logger;
@@ -46,7 +47,7 @@ public class Terrain {
// Configure PLANAR zones to use the same 16x16 pixel image
// that all similar terrains share. (See JSON)
if (this.zone.template.terrain_type.equals("PLANAR"))
if (this.zone.template.terrain_type == mbEnums.TerrainGenType.PLANAR)
this.heightmap = 1006300; // all 0
// Load pixel data for this terrain from cache
@@ -71,8 +72,8 @@ public class Terrain {
// the blending area between child and parent terrains when
// they are stitched together.
this.blend_values.x = this.zone.template.max_blend;
this.blend_values.y = this.zone.template.min_blend;
this.blend_values.x = this.zone.template.zone_max_blend;
this.blend_values.y = this.zone.template.zone_min_blend;
Vector2f major_blend = new Vector2f(this.blend_values.x / this.zone.major_radius,
this.blend_values.y / this.zone.major_radius);
@@ -217,8 +218,8 @@ public class Terrain {
// Normalize terrain offset
Vector2f normalizedOffset = new Vector2f(Math.abs(zone_offset.x) / this.zone.template.major_radius,
Math.abs(zone_offset.y) / this.zone.template.minor_radius);
Vector2f normalizedOffset = new Vector2f(Math.abs(zone_offset.x) / this.zone.template.zone_major_radius,
Math.abs(zone_offset.y) / this.zone.template.zone_minor_radius);
float blendCoefficient;
+1 -1
View File
@@ -280,7 +280,7 @@ public class PvpRecord extends DataRecord {
outStatement.setString(7, DataWarehouse.hasher.encrypt(victimGuild.getNation().getObjectUUID()));
outStatement.setInt(8, this.victim.getLevel());
outStatement.setString(9, DataWarehouse.hasher.encrypt(zone.getObjectUUID()));
outStatement.setString(9, DataWarehouse.hasher.encryptObjectID(zone.getObjectUUID()));
outStatement.setString(10, zone.zoneName);
outStatement.setFloat(11, this.location.getX());
outStatement.setFloat(12, -this.location.getZ()); // flip sign on 'y' coordinate
+129 -43
View File
@@ -28,6 +28,7 @@ import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
public class dbBuildingHandler extends dbHandlerBase {
@@ -37,56 +38,55 @@ public class dbBuildingHandler extends dbHandlerBase {
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public Building CREATE_BUILDING(int parentZoneID, int OwnerUUID, String name, int meshUUID,
Vector3fImmutable location, float meshScale, int currentHP,
ProtectionState protectionState, int currentGold, int rank,
DateTime upgradeDate, int blueprintUUID, float w, float rotY) {
public int PERSIST(Building building) {
Building building = null;
int objectUUID = 0;
if (DbManager.refuseStaticWrite(building))
return 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL `building_CREATE`(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,? ,? ,?, ?);")) {
preparedStatement.setInt(1, parentZoneID);
preparedStatement.setInt(2, OwnerUUID);
preparedStatement.setString(3, name);
preparedStatement.setInt(4, meshUUID);
preparedStatement.setFloat(5, location.x);
preparedStatement.setFloat(6, location.y);
preparedStatement.setFloat(7, location.z);
preparedStatement.setFloat(8, meshScale);
preparedStatement.setInt(9, currentHP);
preparedStatement.setString(10, protectionState.name());
preparedStatement.setInt(11, currentGold);
preparedStatement.setInt(12, rank);
preparedStatement.setInt(1, building.parentZoneUUID);
preparedStatement.setInt(2, building.getOwnerUUID());
preparedStatement.setString(3, building.getName());
preparedStatement.setInt(4, building.meshUUID);
preparedStatement.setFloat(5, building.statLat);
preparedStatement.setFloat(6, building.statAlt);
preparedStatement.setFloat(7, building.statLon);
preparedStatement.setFloat(8, building.getMeshScale().x);
preparedStatement.setInt(9, (int) building.getHealth());
preparedStatement.setString(10, building.getProtectionState().name());
preparedStatement.setInt(11, building.getStrongboxValue());
preparedStatement.setInt(12, building.getRank());
if (upgradeDate != null)
preparedStatement.setTimestamp(13, new java.sql.Timestamp(upgradeDate.getMillis()));
if (building.upgradeDateTime != null)
preparedStatement.setTimestamp(13, java.sql.Timestamp.valueOf(building.upgradeDateTime));
else
preparedStatement.setNull(13, java.sql.Types.DATE);
preparedStatement.setInt(14, blueprintUUID);
preparedStatement.setFloat(15, w);
preparedStatement.setFloat(16, rotY);
preparedStatement.setInt(14, building.getBlueprintUUID());
preparedStatement.setFloat(15, building.getRotation().w);
preparedStatement.setFloat(16, building.getRotation().y);
ResultSet rs = preparedStatement.executeQuery();
if (rs.next()) {
int objectUUID = (int) rs.getLong("UID");
if (objectUUID > 0)
building = GET_BUILDINGBYUUID(objectUUID);
}
if (rs.next())
objectUUID = (int) rs.getLong("UID");
} catch (SQLException e) {
throw new RuntimeException(e);
Logger.error(e);
}
return building;
return objectUUID;
}
public boolean DELETE_FROM_DATABASE(final Building b) {
if (DbManager.refuseStaticWrite(b))
return false;
return removeFromBuildings(b);
}
@@ -107,6 +107,26 @@ public class dbBuildingHandler extends dbHandlerBase {
return buildings;
}
public HashSet<Integer> GET_NPC_BUILDING_OWNERS() {
HashSet<Integer> npcOwners = new HashSet<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT DISTINCT `obj_building`.`ownerUUID` FROM `obj_building` INNER JOIN `object` ON `object`.`UID` = `obj_building`.`ownerUUID` WHERE `object`.`type` = 'npc';")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
npcOwners.add(rs.getInt("ownerUUID"));
} catch (SQLException e) {
Logger.error(e);
}
Logger.info("read: " + npcOwners.size() + " NPC building owners");
return npcOwners;
}
public Building GET_BUILDINGBYUUID(int uuid) {
if (uuid == 0)
@@ -157,6 +177,9 @@ public class dbBuildingHandler extends dbHandlerBase {
String result = "";
if (DbManager.refuseStaticWrite(b))
return result;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL building_SETPROP(?,?,?)")) {
@@ -180,6 +203,9 @@ public class dbBuildingHandler extends dbHandlerBase {
int rowCount;
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `object` INNER JOIN `obj_building` On `object`.`UID` = `obj_building`.`UID` SET `object`.`parent`=?, `obj_building`.`locationX`=?, `obj_building`.`locationY`=?, `obj_building`.`locationZ`=? WHERE `obj_building`.`UID`=?;")) {
@@ -200,23 +226,14 @@ public class dbBuildingHandler extends dbHandlerBase {
}
private boolean removeFromBuildings(final Building b) {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `object` WHERE `UID` = ?")) {
preparedStatement.setLong(1, b.getObjectUUID());
preparedStatement.execute();
return true;
} catch (SQLException e) {
Logger.error(e);
}
return false;
return deleteObjectTree(b.getObjectUUID(), null) >= 0;
}
public boolean CHANGE_NAME(Building b, String newName) {
if (DbManager.refuseStaticWrite(b))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `name`=? WHERE `UID`=?")) {
@@ -233,6 +250,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean SET_RESERVE(Building b, int reserveAmount) {
if (DbManager.refuseStaticWrite(b))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `reserve`=? WHERE `UID`=?")) {
@@ -251,6 +271,9 @@ public class dbBuildingHandler extends dbHandlerBase {
//CAS update to rank
public boolean CHANGE_RANK(final long buildingID, int newRank) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `rank`=? WHERE `UID`=?")) {
@@ -267,6 +290,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean UPDATE_BUILDING_HEALTH(final long buildingID, int NewHealth) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `currentHP`=? WHERE `UID`=?")) {
@@ -283,6 +309,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean UPDATE_BUILDING_ALTITUDE(final long buildingID, float newAltitude) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `locationY`=? WHERE `UID`=?")) {
@@ -299,6 +328,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean UPDATE_PROTECTIONSTATE(final long buildingUUID, ProtectionState protectionState) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingUUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `protectionState`=? WHERE `UID`=?")) {
@@ -315,6 +347,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean UPDATE_DOOR_LOCK(final int buildingUUID, int doorFlags) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingUUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_building SET doorState = ? WHERE UID = ?")) {
@@ -331,6 +366,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean ADD_TO_FRIENDS_LIST(final long buildingID, final long friendID, final long guildID, final int friendType) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_building_friends` (`buildingUID`, `playerUID`,`guildUID`, `friendType`) VALUES (?,?,?,?)")) {
@@ -349,6 +387,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean REMOVE_FROM_FRIENDS_LIST(final long buildingID, long friendID, long guildID, int friendType) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_building_friends` WHERE `buildingUID`=? AND `playerUID`=? AND `guildUID` =? AND `friendType` = ?")) {
@@ -367,6 +408,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean REMOVE_FROM_CONDEMNED_LIST(final long buildingID, long friendID, long guildID, int friendType) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_building_condemned` WHERE `buildingUID`=? AND `playerUID`=? AND `guildUID` =? AND `friendType` = ?")) {
@@ -385,6 +429,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public void CLEAR_FRIENDS_LIST(final long buildingID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_building_friends` WHERE `buildingUID`=?")) {
@@ -399,6 +446,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public void CLEAR_CONDEMNED_LIST(final long buildingID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_building_condemned` WHERE `buildingUID`=?")) {
@@ -413,6 +463,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean CLEAR_PATROL(final long buildingID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, buildingID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_building_patrol_points` WHERE `buildingUID`=?")) {
@@ -514,6 +567,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean ADD_TO_CONDEMNLIST(final long parentUID, final long playerUID, final long guildID, final int friendType) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, parentUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_building_condemned` (`buildingUID`, `playerUID`,`guildUID`, `friendType`) VALUES (?,?,?,?)")) {
@@ -532,6 +588,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean ADD_TO_PATROL(final long parentUID, final Vector3fImmutable patrolPoint) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, parentUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_building_patrol_points` (`buildingUID`, `patrolX`,`patrolY`, `patrolZ`) VALUES (?,?,?,?)")) {
@@ -677,6 +736,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean updateBuildingRank(final Building b, int Rank) {
if (DbManager.refuseStaticWrite(b))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `rank`=?,"
+ "`upgradeDate`=?, `meshUUID`=?, `currentHP`=? "
@@ -698,6 +760,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean updateReverseKOS(final Building b, boolean reverse) {
if (DbManager.refuseStaticWrite(b))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `reverseKOS`=? WHERE `UID` = ?")) {
@@ -714,6 +779,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean updateActiveCondemn(final Condemned condemn, boolean active) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Building, condemn.buildingUUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `dyn_building_condemned` SET `active`=? "
+ "WHERE`buildingUID` = ? AND `playerUID` = ? AND `guildUID` = ? AND `friendType` = ?")) {
@@ -734,6 +802,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean updateBuildingOwner(final Building building, int ownerUUID) {
if (DbManager.refuseStaticWrite(building))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_building` SET `ownerUUID`=? "
+ " WHERE `UID` = ?")) {
@@ -751,6 +822,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean updateBuildingUpgradeTime(LocalDateTime upgradeDateTime, Building toUpgrade, int costToUpgrade) {
if (DbManager.refuseStaticWrite(toUpgrade))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_building SET upgradeDate=?, currentGold=? "
+ "WHERE UID = ?")) {
@@ -774,6 +848,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean updateMaintDate(Building building) {
if (DbManager.refuseStaticWrite(building))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_building SET maintDate=? "
+ "WHERE UID = ?")) {
@@ -795,6 +872,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean addTaxes(Building building, TaxType taxType, int amount, boolean enforceKOS) {
if (DbManager.refuseStaticWrite(building))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_building SET taxType=?, taxAmount = ?, enforceKOS = ? "
+ "WHERE UID = ?")) {
@@ -814,6 +894,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean removeTaxes(Building building) {
if (DbManager.refuseStaticWrite(building))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_building SET taxType=?, taxAmount = ?, enforceKOS = ?, taxDate = ? "
+ "WHERE UID = ?")) {
@@ -834,6 +917,9 @@ public class dbBuildingHandler extends dbHandlerBase {
public boolean acceptTaxes(Building building) {
if (DbManager.refuseStaticWrite(building))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_building SET taxDate=? "
+ "WHERE UID = ?")) {
+18
View File
@@ -139,6 +139,9 @@ public class dbCityHandler extends dbHandlerBase {
public boolean updateforceRename(City city, boolean value) {
if (DbManager.refuseStaticWrite(city))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_city` SET `forceRename`=?"
+ " WHERE `UID` = ?")) {
@@ -156,6 +159,9 @@ public class dbCityHandler extends dbHandlerBase {
public boolean updateOpenCity(City city, boolean value) {
if (DbManager.refuseStaticWrite(city))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_city` SET `open`=?"
+ " WHERE `UID` = ?")) {
@@ -173,6 +179,9 @@ public class dbCityHandler extends dbHandlerBase {
public boolean updateTOL(City city, int tolID) {
if (DbManager.refuseStaticWrite(city))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_city` SET `treeOfLifeUUID`=?"
+ " WHERE `UID` = ?")) {
@@ -191,6 +200,9 @@ public class dbCityHandler extends dbHandlerBase {
public boolean renameCity(City city, String name) {
if (DbManager.refuseStaticWrite(city))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_city` SET `name`=?"
+ " WHERE `UID` = ?")) {
@@ -209,6 +221,9 @@ public class dbCityHandler extends dbHandlerBase {
public boolean updateSiegesWithstood(City city, int value) {
if (DbManager.refuseStaticWrite(city))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_city` SET `name`=?"
+ " WHERE `UID` = ?")) {
@@ -227,6 +242,9 @@ public class dbCityHandler extends dbHandlerBase {
public boolean updateRealmTaxDate(City city, LocalDateTime localDateTime) {
if (DbManager.refuseStaticWrite(city))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_city` SET `realmTaxDate` =?"
+ " WHERE `UID` = ?")) {
+42 -7
View File
@@ -95,6 +95,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean ADD_TO_BANISHED_FROM_GUILDLIST(int target, long characterID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, target))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_guild_banishlist` (`GuildID`, `CharacterID`) VALUES (?,?)")) {
@@ -111,6 +114,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean REMOVE_FROM_BANISH_LIST(int target, long characterID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, target))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_guild_banishlist` (`GuildID`, `CharacterID`) VALUES (?,?)")) {
@@ -358,6 +364,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean SET_GUILD_OWNED_CITY(int guildID, int cityID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, guildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_guild` SET `ownedCity`=? WHERE `UID`=?")) {
@@ -374,6 +383,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean SET_GUILD_LEADER(int objectUUID, int guildID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, guildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_guild` SET `leaderUID`=? WHERE `UID`=?")) {
@@ -413,6 +425,9 @@ public class dbGuildHandler extends dbHandlerBase {
Guild guild = null;
GuildTag guildTag = g.getGuildTag();
if (DbManager.refuseStaticWrite(g))
return null;
if (guildTag == null)
return null;
@@ -465,6 +480,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean UPDATE_PARENT(int guildUID, int parentUID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, guildUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `object` SET `parent`=? WHERE `UID`=?")) {
@@ -483,20 +501,19 @@ public class dbGuildHandler extends dbHandlerBase {
int row_count = 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `object` WHERE `UID` = ?")) {
if (DbManager.refuseStaticWrite(guild))
return 0;
preparedStatement.setLong(1, guild.getObjectUUID());
row_count = preparedStatement.executeUpdate();
row_count = Math.max(deleteObjectTree(guild.getObjectUUID(), null), 0);
} catch (SQLException e) {
Logger.error(e);
}
return row_count;
}
public boolean UPDATE_MINETIME(int guildUID, int mineTime) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, guildUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_guild` SET `mineTime`=? WHERE `UID`=?")) {
@@ -564,6 +581,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean updateDatabase(final Guild g) {
if (DbManager.refuseStaticWrite(g))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_guild` SET `name`=?, `backgroundColor01`=?, `backgroundColor02`=?, `symbolColor`=?, `backgroundDesign`=?, `symbol`=?, `charter`=?, `motd`=?, `icMotd`=?, `nationMotd`=?, `leaderUID`=? WHERE `UID`=?")) {
@@ -590,6 +610,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean ADD_TO_ALLIANCE_LIST(final long sourceGuildID, final long targetGuildID, boolean isRecommended, boolean isAlly, String recommender) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, sourceGuildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_guild_allianceenemylist` (`GuildID`, `OtherGuildID`,`isRecommended`, `isAlliance`, `recommender`) VALUES (?,?,?,?,?)")) {
@@ -609,6 +632,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean REMOVE_FROM_ALLIANCE_LIST(final long sourceGuildID, long targetGuildID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, sourceGuildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_guild_allianceenemylist` WHERE `GuildID`=? AND `OtherGuildID`=?")) {
@@ -625,6 +651,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean UPDATE_RECOMMENDED(final long sourceGuildID, long targetGuildID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, sourceGuildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `dyn_guild_allianceenemylist` SET `isRecommended` = ? WHERE `GuildID`=? AND `OtherGuildID`=?")) {
@@ -641,6 +670,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean UPDATE_ALLIANCE(final long sourceGuildID, long targetGuildID, boolean isAlly) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, sourceGuildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `dyn_guild_allianceenemylist` SET `isAlliance` = ? WHERE `GuildID`=? AND `OtherGuildID`=?")) {
@@ -657,6 +689,9 @@ public class dbGuildHandler extends dbHandlerBase {
public boolean UPDATE_ALLIANCE_AND_RECOMMENDED(final long sourceGuildID, long targetGuildID, boolean isAlly) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Guild, sourceGuildID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `dyn_guild_allianceenemylist` SET `isRecommended` = ?, `isAlliance` = ? WHERE `GuildID`=? AND `OtherGuildID`=?")) {
+50
View File
@@ -17,7 +17,10 @@ import engine.objects.AbstractGameObject;
import engine.objects.AbstractWorldObject;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
public abstract class dbHandlerBase {
@@ -77,4 +80,51 @@ public abstract class dbHandlerBase {
}
return objectList;
}
// Deletes an object row and every row below it by object.parent, as the dropped ref_parent cascade did
// (World Boot Plan 2.4). typeFilter limits the root row to one object type; null deletes any type.
// Returns the number of root rows deleted.
protected int deleteObjectTree(long objectUID, String typeFilter) {
String descendantSql = "DELETE FROM `object` WHERE `UID` IN (SELECT `UID` FROM (WITH RECURSIVE `tree` AS "
+ "(SELECT `UID` FROM `object` WHERE `parent` = ? UNION ALL "
+ "SELECT `object`.`UID` FROM `object` INNER JOIN `tree` ON `object`.`parent` = `tree`.`UID`) "
+ "SELECT `UID` FROM `tree`) AS `descendants`)";
String rootSql = typeFilter == null ? "DELETE FROM `object` WHERE `UID` = ?"
: "DELETE FROM `object` WHERE `UID` = ? AND `type` = ?";
int rootCount = 0;
try (Connection connection = DbManager.getConnection()) {
connection.setAutoCommit(false);
try (PreparedStatement descendantStatement = connection.prepareStatement(descendantSql);
PreparedStatement rootStatement = connection.prepareStatement(rootSql)) {
descendantStatement.setLong(1, objectUID);
descendantStatement.executeUpdate();
rootStatement.setLong(1, objectUID);
if (typeFilter != null)
rootStatement.setString(2, typeFilter);
rootCount = rootStatement.executeUpdate();
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
} catch (SQLException e) {
Logger.error(e);
return -1;
}
return rootCount;
}
}
+24 -7
View File
@@ -32,6 +32,9 @@ public class dbMobHandler extends dbHandlerBase {
Mob mobile = null;
if (DbManager.refuseStaticWrite(toAdd))
return null;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL `mob_CREATE`(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);")) {
@@ -84,6 +87,9 @@ public class dbMobHandler extends dbHandlerBase {
public boolean updateUpgradeTime(Mob mob, DateTime upgradeDateTime) {
if (DbManager.refuseStaticWrite(mob))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_mob SET upgradeDate=? "
+ "WHERE UID = ?")) {
@@ -106,16 +112,12 @@ public class dbMobHandler extends dbHandlerBase {
public int DELETE_MOB(final Mob mob) {
int row_count = 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `object` WHERE `UID` = ?")) {
preparedStatement.setLong(1, mob.getDBID());
row_count = preparedStatement.executeUpdate();
if (DbManager.refuseStaticWrite(mob))
return 0;
} catch (SQLException e) {
Logger.error(e);
row_count = Math.max(deleteObjectTree(mob.getDBID(), null), 0);
}
return row_count;
}
@@ -146,6 +148,9 @@ public class dbMobHandler extends dbHandlerBase {
public boolean ADD_GUARD_MINION(final long captainUID, final String minionName) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Mob, captainUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_guards` (`captainUID`, `minionName`) VALUES (?,?)")) {
@@ -162,6 +167,9 @@ public class dbMobHandler extends dbHandlerBase {
public boolean REMOVE_GUARD_MINION(final long captainUID, final String minionName) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Mob, captainUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_guards` WHERE `captainUID`=? AND `minionName`=? LIMIT 1;")) {
@@ -178,6 +186,9 @@ public class dbMobHandler extends dbHandlerBase {
public boolean REMOVE_ALL_MINIONS(final long captainUID) {
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Mob, captainUID))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `dyn_guards` WHERE `captainUID`=?;")) {
@@ -212,6 +223,9 @@ public class dbMobHandler extends dbHandlerBase {
int row_count = 0;
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.Mob, mobID))
return 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `object` INNER JOIN `obj_mob` On `object`.`UID` = `obj_mob`.`UID` SET `object`.`parent`=?, `obj_mob`.`mob_spawnX`=?, `obj_mob`.`mob_spawnY`=?, `obj_mob`.`mob_spawnZ`=? WHERE `obj_mob`.`UID`=?;")) {
@@ -234,6 +248,9 @@ public class dbMobHandler extends dbHandlerBase {
String result = "";
if (DbManager.refuseStaticWrite(m))
return result;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL mob_SETPROP(?,?,?)")) {
+36 -8
View File
@@ -75,6 +75,9 @@ public class dbNPCHandler extends dbHandlerBase {
NPC npc = null;
if (DbManager.refuseStaticWrite(toAdd))
return null;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL `npc_CREATE`(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);")) {
@@ -120,16 +123,11 @@ public class dbNPCHandler extends dbHandlerBase {
npc.removeFromZone();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `object` WHERE `UID` = ?")) {
if (DbManager.refuseStaticWrite(npc))
return 0;
preparedStatement.setLong(1, npc.getDBID());
row_count = preparedStatement.executeUpdate();
row_count = Math.max(deleteObjectTree(npc.getDBID(), null), 0);
} catch (SQLException e) {
Logger.error(e);
}
return row_count;
}
@@ -172,6 +170,9 @@ public class dbNPCHandler extends dbHandlerBase {
int rowCount;
if (DbManager.refuseStaticWrite(mbEnums.GameObjectType.NPC, npcID))
return 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `object` INNER JOIN `obj_npc` On `object`.`UID` = `obj_npc`.`UID` SET `object`.`parent`=?, `obj_npc`.`npc_spawnX`=?, `obj_npc`.`npc_spawnY`=?, `obj_npc`.`npc_spawnZ`=? WHERE `obj_npc`.`UID`=?;")) {
@@ -195,6 +196,9 @@ public class dbNPCHandler extends dbHandlerBase {
String result = "";
if (DbManager.refuseStaticWrite(n))
return result;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL npc_SETPROP(?,?,?)")) {
@@ -215,6 +219,9 @@ public class dbNPCHandler extends dbHandlerBase {
public void updateDatabase(final NPC npc) {
if (DbManager.refuseStaticWrite(npc))
return;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_npc SET npc_name=?, npc_contractID=?, npc_typeID=?, npc_guildID=?,"
+ " npc_spawnX=?, npc_spawnY=?, npc_spawnZ=?, npc_level=? ,"
@@ -242,6 +249,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean updateUpgradeTime(NPC npc, DateTime upgradeDateTime) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE obj_npc SET upgradeDate=? "
+ "WHERE UID = ?")) {
@@ -264,6 +274,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean UPDATE_MOBBASE(NPC npc, int mobBaseID) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_npc` SET `npc_raceID`=? WHERE `UID`=?")) {
@@ -280,6 +293,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean UPDATE_EQUIPSET(NPC npc, int equipSetID) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_npc` SET `equipsetID`=? WHERE `UID`=?")) {
@@ -296,6 +312,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean UPDATE_NAME(NPC npc, String name) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_npc` SET `npc_name`=? WHERE `UID`=?")) {
@@ -312,6 +331,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean UPDATE_RACE(NPC npc, int value) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `obj_npc` SET `npc_raceID`=? WHERE `UID`=?")) {
@@ -363,6 +385,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean UPDATE_PROFITS(NPC npc, ProfitType profitType, float value) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE `dyn_npc_profits` SET `" + profitType.dbField + "` = ? WHERE `npcUID`=?")) {
@@ -398,6 +423,9 @@ public class dbNPCHandler extends dbHandlerBase {
public boolean CREATE_PROFITS(NPC npc) {
if (DbManager.refuseStaticWrite(npc))
return false;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO `dyn_npc_profits` (`npcUID`) VALUES (?)")) {
@@ -0,0 +1,105 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
public class dbStaticWorldHandler extends dbHandlerBase {
public dbStaticWorldHandler() {
}
// Table and column come from StaticManifest's fixed list, never from input. -1 means "none" and is skipped.
public HashMap<Integer, Integer> GET_NEGATIVE_REFERENCES(String table, String column) {
HashMap<Integer, Integer> rowsByValue = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `" + column + "` AS `value`, COUNT(*) AS `rows` FROM `"
+ table + "` WHERE `" + column + "` < -1 GROUP BY `" + column + "`;")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
rowsByValue.put(rs.getInt("value"), rs.getInt("rows"));
} catch (SQLException e) {
Logger.error(table + "." + column + ": " + e);
return null;
}
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;
}
// NPC cities still in obj_city; 0 once the 2.5 migration has run, -1 on error.
public int COUNT_NPC_CITY_ROWS() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(*) AS `cities` FROM `obj_city` WHERE `isNpc` = 1;")) {
ResultSet rs = preparedStatement.executeQuery();
if (rs.next())
return rs.getInt("cities");
} catch (SQLException e) {
Logger.error(e);
}
return -1;
}
}
+20 -47
View File
@@ -10,10 +10,8 @@
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.gameManager.ZoneManager;
import engine.mbEnums;
import engine.objects.Zone;
import engine.objects.ZoneTemplate;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
@@ -46,6 +44,23 @@ public class dbZoneHandler extends dbHandlerBase {
return zoneList;
}
public ArrayList<Zone> GET_PLAYER_CITY_ZONES() {
ArrayList<Zone> zoneList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `obj_zone`.*, `object`.`parent` FROM `object` INNER JOIN `obj_zone` ON `obj_zone`.`UID` = `object`.`UID` WHERE `obj_zone`.`template` = 0 ORDER BY `object`.`UID` ASC;")) {
ResultSet rs = preparedStatement.executeQuery();
zoneList = getObjectsFromRs(rs, 200);
} catch (SQLException e) {
Logger.error(e);
}
return zoneList;
}
public Zone GET_BY_UID(long ID) {
Zone zone = (Zone) DbManager.getFromCache(mbEnums.GameObjectType.Zone, (int) ID);
@@ -68,54 +83,12 @@ public class dbZoneHandler extends dbHandlerBase {
return zone;
}
public void LOAD_ALL_ZONE_TEMPLATES() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_zone_templates")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
ZoneTemplate zoneTemplate = new ZoneTemplate(rs);
ZoneManager._zone_templates.put(zoneTemplate.templateID, zoneTemplate);
}
// Add player city
ZoneTemplate zoneTemplate = new ZoneTemplate();
zoneTemplate.templateID = 0;
zoneTemplate.sea_level_type = "PARENT";
zoneTemplate.sea_level = 0;
zoneTemplate.max_blend = 128;
zoneTemplate.min_blend = 128;
zoneTemplate.terrain_max_y = 5;
zoneTemplate.major_radius = (int) mbEnums.CityBoundsType.ZONE.halfExtents;
zoneTemplate.minor_radius = (int) mbEnums.CityBoundsType.ZONE.halfExtents;
zoneTemplate.terrain_type = "PLANAR";
ZoneManager._zone_templates.put(zoneTemplate.templateID, zoneTemplate);
} catch (SQLException e) {
Logger.error(e);
}
}
public boolean DELETE_ZONE(final Zone zone) {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM `object` WHERE `UID` = ? AND `type` = 'zone'")) {
preparedStatement.setInt(1, zone.getObjectUUID());
return (preparedStatement.executeUpdate() > 0);
} catch (SQLException e) {
Logger.error(e);
}
if (DbManager.refuseStaticWrite(zone))
return false;
return deleteObjectTree(zone.getObjectUUID(), "zone") > 0;
}
}
+25
View File
@@ -13,6 +13,7 @@ import engine.gameManager.ChatManager;
import engine.gameManager.DbManager;
import engine.mbEnums.GameObjectType;
import engine.objects.*;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
@@ -99,12 +100,18 @@ public abstract class AbstractDevCmd {
if (args.length > 0 && args[0].equalsIgnoreCase("?")) {
this.sendHelp(pcSender);
this.sendUsage(pcSender);
} else if (this.addsWorldContent()) {
this.refuseWorldChange(pcSender, target);
} else {
this.tr = target;
this._doCmd(pcSender, args, target);
}
}
protected boolean addsWorldContent() {
return false;
}
protected abstract void _doCmd(PlayerCharacter pcSender, String[] args,
AbstractGameObject target);
@@ -178,4 +185,22 @@ public abstract class AbstractDevCmd {
ChatManager.chatSystemInfo(pc, msgText);
}
// Static world objects are built from client data each boot, so they are changed with external tooling only.
protected boolean refuseStaticTarget(PlayerCharacter pc, AbstractGameObject target) {
if (!(target instanceof AbstractWorldObject) || !((AbstractWorldObject) target).isStaticWorldObject())
return false;
this.refuseWorldChange(pc, target);
return true;
}
protected void refuseWorldChange(PlayerCharacter pc, AbstractGameObject target) {
String targetText = (target == null) ? "no target" : target.getObjectType() + " " + target.getObjectUUID();
this.throwbackError(pc, "Must use external tooling to modify gameworld");
Logger.info("Refused dev command " + this.getMainCmdString() + " on " + targetText + " by " + pc.getFirstName());
}
}
+11 -12
View File
@@ -14,7 +14,7 @@ import engine.devcmd.AbstractDevCmd;
import engine.gameManager.ChatManager;
import engine.gameManager.DbManager;
import engine.gameManager.ZoneManager;
import engine.math.Vector3f;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums.ProtectionState;
import engine.objects.*;
@@ -54,8 +54,7 @@ public class AddBuildingCmd extends AbstractDevCmd {
"Invalid addBuilding Command. Invalid Building ID.");
return;
}
Vector3f rot = new Vector3f(0.0f, 0.0f, 0.0f);
float w = 1f;
Quaternion rotation = new Quaternion();
Zone zone = ZoneManager.findSmallestZone(pc.getLoc());
if (zone == null) {
@@ -73,8 +72,7 @@ public class AddBuildingCmd extends AbstractDevCmd {
Building likeBuilding = DbManager.BuildingQueries.GET_BUILDING_BY_MESH(ID);
if (likeBuilding != null) {
rot = likeBuilding.getRot();
w = likeBuilding.getw();
rotation = likeBuilding.getRotation();
}
String buildingName = "";
@@ -90,11 +88,10 @@ public class AddBuildingCmd extends AbstractDevCmd {
blueprintUUID = blueprint.getBlueprintUUID();
}
Building building = DbManager.BuildingQueries.
CREATE_BUILDING(
zone.getObjectUUID(), 0, buildingName, ID,
Building building = Building.persistBuilding(Building.createBuilding(
zone, 0, buildingName, ID,
localLoc, 1.0f, 0, ProtectionState.PROTECTED, 0, rank,
null, blueprintUUID, w, rot.y);
null, blueprintUUID, rotation.w, rotation.y));
if (building == null) {
@@ -102,9 +99,6 @@ public class AddBuildingCmd extends AbstractDevCmd {
return;
}
building.setRot(rot);
building.setw(w);
building.setObjectTypeMask(MBServerStatics.MASK_BUILDING);
WorldGrid.addObject(building, pc);
ChatManager.chatSayInfo(pc,
@@ -114,6 +108,11 @@ public class AddBuildingCmd extends AbstractDevCmd {
}
@Override
protected boolean addsWorldContent() {
return true;
}
@Override
protected String _getHelpString() {
return "Creates a building of type 'buildingID' at the location your character is standing.";
+5
View File
@@ -75,6 +75,11 @@ public class AddMobCmd extends AbstractDevCmd {
}
}
@Override
protected boolean addsWorldContent() {
return true;
}
@Override
protected String _getHelpString() {
return "Creates a Mob of type 'mobID' at the location your character is standing";
+5
View File
@@ -94,6 +94,11 @@ public class AddNPCCmd extends AbstractDevCmd {
}
}
@Override
protected boolean addsWorldContent() {
return true;
}
@Override
protected String _getHelpString() {
return "Creates an NPC of type 'npcID' at the location your character is standing";
+3 -3
View File
@@ -37,8 +37,8 @@ public class GetHeightCmd extends AbstractDevCmd {
Vector2f childZoneLoc = ZoneManager.worldToTerrainSpace(playerCharacter.getLoc(), heightmapZone);
Vector2f childZoneOffset = ZoneManager.worldToZoneOffset(playerCharacter.getLoc(), heightmapZone);
Vector2f normalizedOffset = new Vector2f(Math.abs(childZoneOffset.x) / heightmapZone.template.major_radius,
Math.abs(childZoneOffset.y) / heightmapZone.template.minor_radius);
Vector2f normalizedOffset = new Vector2f(Math.abs(childZoneOffset.x) / heightmapZone.template.zone_major_radius,
Math.abs(childZoneOffset.y) / heightmapZone.template.zone_minor_radius);
Vector2f parentZoneLoc = ZoneManager.worldToTerrainSpace(playerCharacter.getLoc(), parentZone);
float childHeight = heightmapZone.terrain.getInterpolatedTerrainHeight(childZoneLoc);
@@ -74,7 +74,7 @@ public class GetHeightCmd extends AbstractDevCmd {
this.throwbackInfo(playerCharacter, "Child Zone Offset: " + "[" + childZoneOffset.x + "]" + "[" + childZoneOffset.y + "]");
this.throwbackInfo(playerCharacter, "Normalized offset: " + "[" + normalizedOffset.x + "]" + "[" + normalizedOffset.y + "]");
this.throwbackInfo(playerCharacter, "template blend Values: (max/min): " + heightmapZone.template.max_blend + " /" + heightmapZone.template.min_blend);
this.throwbackInfo(playerCharacter, "template blend Values: (max/min): " + heightmapZone.template.zone_max_blend + " /" + heightmapZone.template.zone_min_blend);
this.throwbackInfo(playerCharacter, "terrain values (max/min): " + heightmapZone.terrain.blend_values.x + " /" + heightmapZone.terrain.blend_values.y);
this.throwbackInfo(playerCharacter, "Blend coefficient: " + heightmapZone.terrain.getTerrainBlendCoefficient(childZoneOffset));
+13 -4
View File
@@ -19,6 +19,7 @@ import engine.mbEnums.GameObjectType;
import engine.mbEnums.TargetColor;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.server.MBServerStatics;
import engine.util.StringUtils;
import java.text.DecimalFormat;
@@ -88,7 +89,7 @@ public class InfoCmd extends AbstractDevCmd {
output += newline;
output += StringUtils.addWS("Alt: " + targetLoc.y, 20);
output += newline;
output += "Rot: " + targetAWO.getRot().y;
output += "Rot: " + targetAWO.getRotation().y;
output += newline;
double radian = 0;
@@ -106,7 +107,7 @@ public class InfoCmd extends AbstractDevCmd {
case Building:
Building targetBuilding = (Building) target;
output += StringUtils.addWS("Lac: "
+ targetBuilding.getw(), 20);
+ targetBuilding.getRotation().w, 20);
output += "Blueprint : ";
output += targetBuilding.getBlueprintUUID();
output += newline;
@@ -343,7 +344,7 @@ public class InfoCmd extends AbstractDevCmd {
case NPC:
NPC targetNPC = (NPC) target;
output += "databaseID: " + targetNPC.getDBID() + newline;
output += "databaseID: " + databaseIDText(targetNPC.getDBID()) + newline;
output += "Name: " + targetNPC.getName();
output += newline;
output += StringUtils.addWS("Level: " + targetNPC.getLevel(), 20);
@@ -414,7 +415,7 @@ public class InfoCmd extends AbstractDevCmd {
break;
case Mob:
Mob targetMob = (Mob) target;
output += "databaseID: " + targetMob.getDBID() + newline;
output += "databaseID: " + databaseIDText(targetMob.getDBID()) + newline;
output += "Name: " + targetMob.getName();
output += newline;
output += StringUtils.addWS("Level: " + targetMob.getLevel(), 20);
@@ -539,6 +540,14 @@ public class InfoCmd extends AbstractDevCmd {
throwbackInfo(pc, output);
}
private static String databaseIDText(int databaseID) {
if (databaseID == MBServerStatics.NO_DB_ROW_ASSIGNED_YET)
return "none";
return String.valueOf(databaseID);
}
@Override
protected String _getHelpString() {
return "Gets information on an Object.";
+3
View File
@@ -66,6 +66,9 @@ public class PullCmd extends AbstractDevCmd {
return;
}
if (this.refuseStaticTarget(pcSender, target))
return;
targetLoc = pcSender.getLoc();
serverZone = ZoneManager.findSmallestZone(targetLoc);
switch (target.getObjectType()) {
+8 -1
View File
@@ -245,11 +245,17 @@ public class PurgeObjectsCmd extends AbstractDevCmd {
HashSet<AbstractWorldObject> objectList =
WorldGrid.getObjectsInRangePartial(_currentLocation, _targetRange, _targetMask);
int staticSkipped = 0;
// Iterate through array and remove objects from game world and database
for (AbstractWorldObject awo : objectList) {
if (awo.isStaticWorldObject()) {
staticSkipped++;
continue;
}
switch (awo.getObjectType()) {
case Building:
removeBuilding(pc, (Building) awo);
@@ -266,7 +272,8 @@ public class PurgeObjectsCmd extends AbstractDevCmd {
}
// Send results to user
throwbackInfo(pc, "Purge: " + objectList.size() + " objects were removed in range " + _targetRange);
throwbackInfo(pc, "Purge: " + (objectList.size() - staticSkipped) + " objects were removed in range " + _targetRange
+ ", " + staticSkipped + " static objects skipped");
} catch (Exception e) {
Logger.error(e);
}
@@ -119,6 +119,9 @@ public class RemoveObjectCmd extends AbstractDevCmd {
private void removeBuilding(PlayerCharacter pc, Building building) {
if (this.refuseStaticTarget(pc, building))
return;
Zone currentZone = ZoneManager.findSmallestZone(pc.getLoc());
if (currentZone == null) {
@@ -164,6 +167,9 @@ public class RemoveObjectCmd extends AbstractDevCmd {
private void removeNPC(PlayerCharacter pc, NPC npc) {
if (this.refuseStaticTarget(pc, npc))
return;
Zone currentZone = ZoneManager.findSmallestZone(pc.getLoc());
if (currentZone == null) {
this.throwbackError(pc, "Could not locate zone for player.");
@@ -196,6 +202,9 @@ public class RemoveObjectCmd extends AbstractDevCmd {
private void removeMob(PlayerCharacter pc, Mob mob) {
if (this.refuseStaticTarget(pc, mob))
return;
Zone currentZone = ZoneManager.findSmallestZone(pc.getLoc());
if (currentZone == null) {
+4
View File
@@ -47,6 +47,10 @@ public class RenameCmd extends AbstractDevCmd {
building = (Building) target;
} else
npc = getTargetAsNPC(pcSender);
if (this.refuseStaticTarget(pcSender, npc) || this.refuseStaticTarget(pcSender, building))
return;
if (npc != null) {
DbManager.NPCQueries.SET_PROPERTY(npc, "npc_name", args[0]);
String name = args[0];
+39 -37
View File
@@ -14,7 +14,8 @@ import engine.devcmd.AbstractDevCmd;
import engine.gameManager.BuildingManager;
import engine.gameManager.ChatManager;
import engine.gameManager.DbManager;
import engine.math.Vector3f;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.objects.*;
@@ -63,14 +64,12 @@ public class RotateCmd extends AbstractDevCmd {
return;
}
Vector3f rotation = new Vector3f(0f, rot, 0f);
if (target instanceof Building)
rotateBuilding(pc, (Building) target, rotation, rot, false);
rotateBuilding(pc, (Building) target, (float) Math.toRadians(rot));
else if (target instanceof NPC)
rotateNPC(pc, (NPC) target, rotation, false);
rotateNPC(pc, (NPC) target, (float) Math.toRadians(rot));
else if (target instanceof Mob)
rotateMob(pc, (Mob) target, rotation, false);
rotateMob(pc, (Mob) target, (float) Math.toRadians(rot));
else
throwbackError(pc, "Target " + target.getObjectType()
+ " is not a valid object type");
@@ -141,15 +140,13 @@ public class RotateCmd extends AbstractDevCmd {
}
}
Vector3f rotation = new Vector3f(0f, rot, 0f);
Building b;
if (id != 0)
b = BuildingManager.getBuilding(id);
else
b = getTargetAsBuilding(pc);
if (b != null) {
rotateBuilding(pc, b, rotation, rot, false);
rotateBuilding(pc, b, (float) Math.toRadians(rot));
return;
}
@@ -160,7 +157,7 @@ public class RotateCmd extends AbstractDevCmd {
else
npc = getTargetAsNPC(pc);
if (npc != null) {
rotateNPC(pc, npc, rotation, false);
rotateNPC(pc, npc, (float) Math.toRadians(rot));
return;
}
@@ -171,7 +168,7 @@ public class RotateCmd extends AbstractDevCmd {
else
mob = getTargetAsMob(pc);
if (mob != null) {
rotateMob(pc, mob, rotation, false);
rotateMob(pc, mob, (float) Math.toRadians(rot));
return;
}
throwbackError(pc, "Nothing found to rotate.");
@@ -185,40 +182,52 @@ public class RotateCmd extends AbstractDevCmd {
@Override
protected String _getUsageString() {
return "' /rotate [objectID] rotation' || ' /rot [objectID] rotation'";
return "' /rotate [objectID] degrees' || ' /rot [objectID] degrees' || ' /rotate face'";
}
private void rotateBuilding(PlayerCharacter pc, Building building, Vector3f rot, float orig, boolean faceDirection) {
if (!faceDirection)
rot.set(0.0f, (float) Math.sin(Math.toRadians(orig) / 2), 0.0f);
building.setRot(rot);
building.setw((float) Math.abs(Math.cos(Math.toRadians(orig) / 2)));
private void rotateBuilding(PlayerCharacter pc, Building building, float yaw) {
if (this.refuseStaticTarget(pc, building))
return;
building.setRotation(Quaternion.fromYaw(yaw));
building.getBounds().setBounds(building);
WorldGrid.updateObject(building, pc);
DbManager.BuildingQueries.SET_PROPERTY(building, "rotY", building.getRot().getY());
DbManager.BuildingQueries.SET_PROPERTY(building, "w", building.getw());
DbManager.BuildingQueries.SET_PROPERTY(building, "rotY", building.getRotation().y);
DbManager.BuildingQueries.SET_PROPERTY(building, "w", building.getRotation().w);
ChatManager.chatSayInfo(pc,
"Building with ID " + building.getObjectUUID() + " rotated");
}
private void rotateNPC(PlayerCharacter pc, NPC npc, Vector3f rot, boolean faceDirection) {
npc.setRot(rot);
DbManager.NPCQueries.SET_PROPERTY(npc, "npc_rotation", rot.y);
private void rotateNPC(PlayerCharacter pc, NPC npc, float yaw) {
if (this.refuseStaticTarget(pc, npc))
return;
npc.setRotation(characterRotation(yaw));
DbManager.NPCQueries.SET_PROPERTY(npc, "npc_rotation", npc.getRotation().w);
WorldGrid.updateObject(npc, pc);
//no rotation for npc's in db currently
ChatManager.chatSayInfo(pc,
"NPC with ID " + npc.getObjectUUID() + " rotated");
}
private void rotateMob(PlayerCharacter pc, Mob mob, Vector3f rot, boolean faceDirection) {
mob.setRot(rot);
DbManager.MobQueries.SET_PROPERTY(mob, "mob_rotation", rot.y);
private void rotateMob(PlayerCharacter pc, Mob mob, float yaw) {
if (this.refuseStaticTarget(pc, mob))
return;
mob.setRotation(characterRotation(yaw));
DbManager.MobQueries.SET_PROPERTY(mob, "mob_rotation", mob.getRotation().w);
WorldGrid.updateObject(mob, pc);
//no rotation for mobs's in db currently
ChatManager.chatSayInfo(pc,
"Mob with ID " + mob.getObjectUUID() + " rotated");
}
// npc_rotation and mob_rotation store w = cos θ/2, which reloads to the same yaw only for θ in [0, 2π).
private static Quaternion characterRotation(float yaw) {
return Quaternion.fromYaw((float) (yaw - FastMath.TWO_PI * Math.floor(yaw / FastMath.TWO_PI)));
}
private void rotateFace(PlayerCharacter pc, AbstractGameObject target) {
AbstractWorldObject awo = (AbstractWorldObject) target;
if (awo == null)
@@ -230,19 +239,12 @@ public class RotateCmd extends AbstractDevCmd {
float rotangle = faceDirection.getRotation();
float rot = (float) Math.toDegrees(rotangle);
if (rot > 180)
rot *= -1;
Vector3f buildingrotation = new Vector3f(0f, rot, 0f);
Vector3f rotation = new Vector3f(0f, rotangle, 0f);
if (target instanceof Building)
rotateBuilding(pc, (Building) target, buildingrotation, rot, false);
rotateBuilding(pc, (Building) target, rotangle);
else if (target instanceof NPC)
rotateNPC(pc, (NPC) target, rotation, true);
rotateNPC(pc, (NPC) target, rotangle);
else if (target instanceof Mob)
rotateMob(pc, (Mob) target, rotation, true);
rotateMob(pc, (Mob) target, rotangle);
else
throwbackError(pc, "Target " + target.getObjectType()
+ " is not a valid object type");
@@ -66,6 +66,9 @@ public class SetBuildingAltitudeCmd extends AbstractDevCmd {
Building targetBuilding = (Building) target;
if (this.refuseStaticTarget(pc, targetBuilding))
return;
float altitude = 0;
try {
@@ -36,6 +36,8 @@ public class SetForceRenameCityCmd extends AbstractDevCmd {
City city = City.getCity(zone.playerCityUUID);
if (city == null)
return;
if (this.refuseStaticTarget(pc, city))
return;
city.setForceRename(rename);
}
+3
View File
@@ -37,6 +37,9 @@ public class SetGuildCmd extends AbstractDevCmd {
targetNPC = getTargetAsNPC(pcSender);
targetGuild = Guild.getGuild(Integer.parseInt(args[0]));
if (this.refuseStaticTarget(pcSender, targetNPC))
return;
DbManager.NPCQueries.SET_PROPERTY(targetNPC, "npc_guildID", args[0]);
targetNPC.setGuild(targetGuild);
@@ -41,6 +41,9 @@ public class SetNpcNameCmd extends AbstractDevCmd {
NPC npc = (NPC) target;
if (this.refuseStaticTarget(pc, npc))
return;
String name = words[0];
NPC.UpdateName(npc, name);
+3
View File
@@ -35,6 +35,9 @@ public class SetOwnerCmd extends AbstractDevCmd {
_targetBuilding = getTargetAsBuilding(pcSender);
if (this.refuseStaticTarget(pcSender, _targetBuilding))
return;
// if it's a tol change ownership of the city
if (_targetBuilding.getBlueprint() != null &&
+7
View File
@@ -51,6 +51,10 @@ public class SetRankCmd extends AbstractDevCmd {
}
if (target != null) {
if (this.refuseStaticTarget(player, target))
return;
switch (target.getObjectType()) {
case Building:
Building targetBuilding = (Building) target;
@@ -99,6 +103,9 @@ public class SetRankCmd extends AbstractDevCmd {
return;
}
if (this.refuseStaticTarget(player, targetBuilding))
return;
Blueprint blueprint = targetBuilding.getBlueprint();
if (blueprint == null) {
+3 -1
View File
@@ -31,6 +31,7 @@ import java.time.ZoneId;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
@@ -44,6 +45,7 @@ public enum BuildingManager {
public static HashMap<Integer, ConcurrentHashMap<Integer, BuildingFriends>> _buildingFriends = new HashMap<>();
public static HashMap<Integer, ConcurrentHashMap<Integer, Condemned>> _buildingCondemned = new HashMap<>();
public static HashMap<Integer, ArrayList<Vector3fImmutable>> _buildingPatrolPoints = new HashMap<>();
public static HashSet<Integer> _npcBuildingOwners = new HashSet<>();
public static int getAvailableSlot(Building building) {
@@ -723,7 +725,7 @@ public enum BuildingManager {
Vector3fImmutable bindLoc = null;
if (building == null)
return mbEnums.Ruins.getRandomRuin().getLocation();
return ZoneManager.getRandomRuinLocation();
bindLoc = building.getLoc();
@@ -71,6 +71,7 @@ public enum ConfigManager {
MB_WORLD_KEYCLONE_MAX,
MB_USE_RUINS,
MB_RULESET,
MB_STATIC_BUILDINGS,
// Mobile AI modifiers
MB_AI_CAST_FREQUENCY,
+52
View File
@@ -23,10 +23,15 @@ import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.EnumMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public enum DbManager {
DBMANAGER;
// Static world IDs count down from -2 (-1 means "not found"); runtime negative IDs start below them.
public static final int RUNTIME_ID_START = -1_000_000;
private static final AtomicInteger lastStaticID = new AtomicInteger(-1);
public static final dbAccountHandler AccountQueries = new dbAccountHandler();
public static final dbBaneHandler BaneQueries = new dbBaneHandler();
@@ -64,12 +69,59 @@ public enum DbManager {
public static final dbBlueprintHandler BlueprintQueries = new dbBlueprintHandler();
public static final dbShrineHandler ShrineQueries = new dbShrineHandler();
public static final dbRunegateHandler RunegateQueries = new dbRunegateHandler();
public static final dbStaticWorldHandler StaticWorldQueries = new dbStaticWorldHandler();
public static final dbPetitionHandler PetitionQueries = new dbPetitionHandler();
private static final EnumMap<GameObjectType, ConcurrentHashMap<Integer, AbstractGameObject>> objectCache = new EnumMap<>(GameObjectType.class);
public static Hasher hasher;
private static HikariDataSource connectionPool = null;
// Only the static world factories may call this, in the fixed load order (W2); never renumber.
// The key names the object from the data files alone (findings §5, manifest format) and must be unique.
public static int nextStaticID(GameObjectType objectType, String manifestKey) {
int staticID = lastStaticID.decrementAndGet();
if (staticID <= RUNTIME_ID_START)
throw new IllegalStateException("Static world ID " + staticID + " reached the runtime ID range at " + RUNTIME_ID_START);
StaticManifest.addEntry(staticID, objectType, manifestKey);
return staticID;
}
// W3: static world objects never reach the DB. Logs the refused handler method and its caller.
public static boolean refuseStaticWrite(AbstractWorldObject worldObject) {
StackTraceElement[] stack;
String caller;
int frame = 1;
if (worldObject == null || !worldObject.isStaticWorldObject())
return false;
stack = new Throwable().getStackTrace();
while (frame < stack.length - 1 && stack[frame].getClassName().equals(DbManager.class.getName()))
frame++;
caller = (frame + 1 < stack.length) ? stack[frame + 1].toString() : "unknown";
Logger.error("Refused DB write for static " + worldObject.getObjectType() + " " + worldObject.getObjectUUID()
+ " in " + stack[frame] + " called from " + caller);
return true;
}
public static boolean refuseStaticWrite(GameObjectType objectType, long objectUUID) {
AbstractGameObject gameObject = getFromCache(objectType, (int) objectUUID);
if (gameObject instanceof AbstractWorldObject)
return refuseStaticWrite((AbstractWorldObject) gameObject);
return false;
}
public static AbstractGameObject getObject(GameObjectType objectType, int objectUUID) {
AbstractGameObject outObject = null;
+5
View File
@@ -131,6 +131,11 @@ public enum ForgeManager implements Runnable {
if (city == null)
return 58; //58: The formula is beyond the means of this facility
// NPC buildings have no strongbox to pay from, as in vendor buy, repair and train
if (workOrder.vendor.building.getProtectionState() == mbEnums.ProtectionState.NPC)
return 58;
// Concurrency is rightly managed by same lock as warehouse
city.transactionLock.writeLock().lock();
+105
View File
@@ -16,7 +16,18 @@ import engine.net.client.ClientConnection;
import engine.net.client.msg.guild.AcceptInviteToGuildMsg;
import engine.net.client.msg.guild.GuildInfoMsg;
import engine.objects.*;
import engine.parsers.map.GuildSeedParser;
import engine.parsers.objects.CZoneProp;
import engine.parsers.objects.GuildSeed;
import engine.parsers.objects.GuildSeedEntry;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.TreeMap;
import java.util.TreeSet;
public enum GuildManager {
@@ -33,6 +44,100 @@ public enum GuildManager {
public static final int CREST_RESERVED = 153; //153: Guild crest is reserved
public static final int CREST_COLOR_ERROR = 154; //154: All three crest colors cannot be the same
// NPC guilds by CZONE city_prop_gm_guild / city_prop_gm_nation key (W7)
public static final HashMap<Integer, Guild> _npcGuildsByKey = new HashMap<>();
public static final HashMap<Integer, Guild> _npcGuildsByNationKey = new HashMap<>();
public static final HashSet<Integer> _noGuildKeys = new HashSet<>();
public static final HashSet<Integer> _ruinKeys = new HashSet<>();
public static final HashMap<Integer, GuildSeedEntry> _npcSeedEntriesByGuildID = new HashMap<>();
// Static NPC guilds from maps/<map>.guild. Loaded before zones, so they take the first static IDs. False aborts boot.
public static boolean loadStaticGuilds(int mapID) {
GuildSeed guildSeed;
Guild guild;
guildSeed = GuildSeedParser.parseGuildSeed(mapID);
if (guildSeed == null)
return false;
for (GuildSeedEntry seedEntry : guildSeed.guilds) {
guild = Guild.createStaticGuild(seedEntry);
DbManager.addToCache(guild);
_npcSeedEntriesByGuildID.put(guild.getObjectUUID(), seedEntry);
for (int guildKey : seedEntry.guildKeys)
if (_npcGuildsByKey.put(guildKey, guild) != null || guildSeed.noGuildKeys.contains(guildKey) || guildSeed.ruinKeys.contains(guildKey)) {
Logger.error("Guild seed: guild key " + String.format("%08X", guildKey) + " is used twice");
return false;
}
if (_npcGuildsByNationKey.put(seedEntry.nationKey, guild) != null) {
Logger.error("Guild seed: nation key " + String.format("%08X", seedEntry.nationKey) + " is used twice");
return false;
}
}
_noGuildKeys.addAll(guildSeed.noGuildKeys);
_noGuildKeys.addAll(guildSeed.ruinKeys);
_ruinKeys.addAll(guildSeed.ruinKeys);
Logger.info("NPC guilds: " + guildSeed.guilds.size() + " static, " + _npcGuildsByKey.size() + " guild keys");
return true;
}
// Logs each CITY-prop guild key on the map that has no seed entry; known no-guild keys are only counted.
public static void logMissingGuildKeys() {
TreeMap<String, TreeSet<String>> missingKeys = new TreeMap<>();
ArrayList<CZoneProp> cityProps = new ArrayList<>();
int noGuildProps = 0;
for (Zone zone : ZoneManager.getAllZones()) {
if (!zone.isStaticWorldObject() || zone.template == null)
continue;
cityProps.clear();
collectCityProps(zone.template.zone_prop_info, cityProps);
for (CZoneProp cityProp : cityProps) {
if (_npcGuildsByKey.containsKey(cityProp.city_prop_gm_guild))
continue;
if (_noGuildKeys.contains(cityProp.city_prop_gm_guild)) {
noGuildProps++;
continue;
}
missingKeys.computeIfAbsent(String.format("%08X", cityProp.city_prop_gm_guild), key -> new TreeSet<>())
.add(zone.zoneName);
}
}
for (String guildKey : missingKeys.keySet())
Logger.warn("CITY prop guild key " + guildKey + " has no guild seed entry; zones: " + missingKeys.get(guildKey));
Logger.info("CITY props: " + noGuildProps + " with no guild, " + missingKeys.size() + " guild keys missing from the seed");
}
private static void collectCityProps(ArrayList<CZoneProp> props, ArrayList<CZoneProp> cityProps) {
for (CZoneProp prop : props) {
if (prop.prop_type == mbEnums.ZonePropType.CITY)
cityProps.add(prop);
collectCityProps(prop.prop_content_props, cityProps);
}
}
public static boolean joinGuild(PlayerCharacter pc, Guild guild, GuildHistoryType historyType) {
return joinGuild(pc, guild, 0, historyType);
}
+2 -1
View File
@@ -20,7 +20,8 @@ import java.util.concurrent.atomic.AtomicInteger;
public enum ItemManager {
ITEMMANAGER;
public static final AtomicInteger lastNegativeID = new AtomicInteger(0);
// Shared by every runtime object with a negative ID: loot, forged and contract items, pets and minions.
public static final AtomicInteger lastNegativeID = new AtomicInteger(DbManager.RUNTIME_ID_START);
// restrict true: the set is a blacklist; false: a whitelist. An empty set allows everyone.
+1 -1
View File
@@ -115,7 +115,7 @@ public enum MovementManager {
// if inside a building, convert both locations from the building local reference frame to the world reference frame
if (msg.getInBuildingUUID() > 0) {
if (msg.getInBuildingUUID() != 0) {
Building building = BuildingManager.getBuildingFromCache(msg.getInBuildingUUID());
if (building != null) {
+4 -6
View File
@@ -11,7 +11,6 @@ package engine.gameManager;
import engine.InterestManagement.WorldGrid;
import engine.loot.WorkOrder;
import engine.math.Quaternion;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.net.Dispatch;
@@ -32,8 +31,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static engine.math.FastMath.acos;
public enum NPCManager {
NPC_MANAGER;
@@ -227,11 +224,12 @@ public enum NPCManager {
abstractCharacter.loc = new Vector3fImmutable(abstractCharacter.bindLoc);
// Rotate NPC rotation by the building's rotation
// Rotate NPC rotation by the building's rotation.
// Kept for wire parity: the product's sin θ/2 is read back as cos θ/2, so the yaw sent is not own + building.
Quaternion slotRotation = new Quaternion().fromAngles(0, acos(abstractCharacter.getRot().y) * 2, 0);
Quaternion slotRotation = new Quaternion().fromAngles(0, abstractCharacter.getRotation().getYaw(), 0);
slotRotation = slotRotation.mult(abstractCharacter.building.getBounds().getQuaternion());
abstractCharacter.setRot(new Vector3f(0, slotRotation.y, 0));
abstractCharacter.setRotation(Quaternion.fromYaw((float) Math.acos(slotRotation.y) * 2));
// Configure region and floor/level for this NPC
@@ -0,0 +1,209 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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, top-level props in JSON order. Each prop's children take their IDs (JSON order) before
// the prop itself, so the parent has the lowest ID of its family, as DB UIDs did. 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<CZoneProp> keptZoneProps = new ArrayList<>();
ArrayList<CZoneProp> keptChildProps = new ArrayList<>();
ArrayList<Integer> keptChildIndexes = new ArrayList<>();
ArrayList<Integer> childIDs = new ArrayList<>();
HashMap<String, Integer> leadersByName;
StaticBuildingEntry entry;
StaticBuildingEntry childEntry;
Zone zone;
CZoneProp prop;
CZoneProp childProp;
String key;
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);
keptZoneProps.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, keptZoneProps)) {
twinCount += 1 + prop.prop_content_props.size();
continue;
}
keptZoneProps.add(prop);
key = "prop:" + treeEntry.uid + "/" + propIndex;
keptChildProps.clear();
keptChildIndexes.clear();
childIDs.clear();
for (int childIndex = 0; childIndex < prop.prop_content_props.size(); childIndex++) {
childProp = prop.prop_content_props.get(childIndex);
if (isStackedTwin(childProp, keptChildProps)) {
twinCount++;
continue;
}
keptChildProps.add(childProp);
keptChildIndexes.add(childIndex);
}
for (int childIndex : keptChildIndexes)
childIDs.add(DbManager.nextStaticID(GameObjectType.Building, key + "/" + childIndex));
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, key, DbManager.nextStaticID(GameObjectType.Building, key), zone, prop, null, ownerUUID, addToWorld);
topLevelEntries.add(entry);
buildingCount++;
for (int i = 0; i < keptChildProps.size(); i++) {
childEntry = buildEntry(treeEntry.uid, key + "/" + keptChildIndexes.get(i), childIDs.get(i), zone, keptChildProps.get(i), 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, int staticID, 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 = staticID;
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<CZoneProp> keptSiblings) {
CZonePlacement placement = prop.prop_basic_zone;
CZonePlacement keptPlacement;
for (CZoneProp kept : keptSiblings) {
keptPlacement = kept.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<>();
}
}
@@ -0,0 +1,412 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
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 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 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 exactCount;
private static int movedCount;
private static int twinCount;
private static float maxChildMove;
// Run after NPCs load, so a DB building's owner resolves to its guild.
public static void run(int mapID) {
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;
topLevelEntries = StaticBuildingLoader.buildStaticBuildings(false);
for (StaticBuildingEntry entry : topLevelEntries) {
entriesByZone.computeIfAbsent(entry.zoneTreeUID, k -> new ArrayList<>()).add(entry);
propCount += 1 + entry.children.size();
}
// 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)
dbTopLevelByZone.computeIfAbsent(zone.getObjectUUID(), k -> new ArrayList<>()).add(building);
else
dbChildrenByParent.computeIfAbsent(building.parentBuildingID, k -> new ArrayList<>()).add(building);
}
// Pass 1: zone, mesh and local x/z.
for (Map.Entry<Integer, ArrayList<StaticBuildingEntry>> zoneEntries : entriesByZone.entrySet())
for (StaticBuildingEntry entry : zoneEntries.getValue()) {
dbBuilding = takeMatch(entry, dbTopLevelByZone.getOrDefault(zoneEntries.getKey(), new ArrayList<>()));
if (dbBuilding == null)
continue;
exactCount++;
mapPair(dbBuilding, entry, true);
}
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);
}
// Pass 3: a DB row of a dropped stacked twin maps to the kept twin.
for (Building building : new ArrayList<>(dbUnmatchedTop)) {
StaticBuildingEntry entry = findTwin(building, entriesByZone.getOrDefault(building.parentZoneUUID, new ArrayList<>()));
if (entry == null)
continue;
dbUnmatchedTop.remove(building);
twinCount++;
mapPair(building, entry, false);
}
logResults(dbStatic, topLevelEntries);
writeMigration(mapID, dbStatic);
differences.clear();
entriesByDbUID.clear();
matchedEntries.clear();
dbChildrenByParent.clear();
}
// 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) {
ArrayList<Building> dbChildren = dbChildrenByParent.getOrDefault(dbBuilding.getObjectUUID(), new ArrayList<>());
ArrayList<Building> dbChildCandidates = new ArrayList<>(dbChildren);
Building dbChild;
StaticBuildingEntry twin;
float childMove;
entriesByDbUID.put(dbBuilding.getObjectUUID(), entry);
if (compare) {
matchedEntries.add(entry);
compareBuilding(entry, dbBuilding);
}
for (StaticBuildingEntry childEntry : entry.children) {
dbChild = takeMatch(childEntry, dbChildCandidates);
if (dbChild == null)
continue;
entriesByDbUID.put(dbChild.getObjectUUID(), childEntry);
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(childEntry.building.getLoc());
if (childMove > POSITION_TOLERANCE) {
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(StaticBuildingEntry entry, ArrayList<Building> candidates) {
Building found = null;
float foundDistance = Float.MAX_VALUE;
float distance;
for (Building candidate : candidates) {
distance = localDistance(candidate, entry);
if (distance <= POSITION_TOLERANCE && distance < foundDistance) {
found = candidate;
foundDistance = distance;
}
}
if (found != null)
candidates.remove(found);
return found;
}
// 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) {
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 = "";
if (!sameRotation(dbBuilding.getRotation(), jsonBuilding.getRotation()))
addDifference("rotation", prefix + " DB " + rotationText(dbBuilding.getRotation()) + " JSON " + rotationText(jsonBuilding.getRotation()));
if (!dbBuilding.getMeshScale().equals(jsonBuilding.getMeshScale()))
addDifference("scale", prefix + " DB " + dbBuilding.getMeshScale() + " JSON " + jsonBuilding.getMeshScale());
if (dbBuilding.rank != jsonBuilding.rank)
addDifference("rank", prefix + " DB " + dbBuilding.rank + " JSON " + jsonBuilding.rank);
if (dbBuilding.getStrongboxValue() != jsonBuilding.getStrongboxValue())
addDifference("strongbox", prefix + " DB " + dbBuilding.getStrongboxValue() + " JSON " + jsonBuilding.getStrongboxValue());
if (!dbBuilding.getName().equals(jsonBuilding.getName()))
addDifference("name", prefix + " DB '" + dbBuilding.getName() + "' JSON '" + jsonBuilding.getName() + "'");
if (dbBuilding.floor != jsonBuilding.floor || dbBuilding.level != jsonBuilding.level)
addDifference("floor/level", prefix + " DB " + dbBuilding.floor + "/" + dbBuilding.level + " JSON " + jsonBuilding.floor + "/" + jsonBuilding.level);
if (dbBuilding.protectionState != jsonBuilding.protectionState)
addDifference("protection", prefix + " DB " + dbBuilding.protectionState + " JSON " + jsonBuilding.protectionState);
if (dbBuilding.isFurniture != jsonBuilding.isFurniture)
addDifference("furniture flag", prefix + " DB " + dbBuilding.isFurniture + " JSON " + jsonBuilding.isFurniture);
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();
if (!dbGuild.equals(jsonGuild))
addDifference("owner guild", prefix + " DB '" + dbGuild + "' JSON '" + jsonGuild + "'");
}
// q and -q are the same rotation.
private static boolean sameRotation(Quaternion a, Quaternion b) {
boolean same = Math.abs(a.w - b.w) < ROTATION_TOLERANCE && Math.abs(a.x - b.x) < ROTATION_TOLERANCE
&& Math.abs(a.y - b.y) < ROTATION_TOLERANCE && Math.abs(a.z - b.z) < ROTATION_TOLERANCE;
boolean negated = Math.abs(a.w + b.w) < ROTATION_TOLERANCE && Math.abs(a.x + b.x) < ROTATION_TOLERANCE
&& Math.abs(a.y + b.y) < ROTATION_TOLERANCE && Math.abs(a.z + b.z) < ROTATION_TOLERANCE;
return same || negated;
}
private static String rotationText(Quaternion rotation) {
return "(w " + rotation.w + ", x " + rotation.x + ", y " + rotation.y + ", z " + rotation.z + ")";
}
private static String guildName(Building building) {
Guild guild;
if (building.getOwnerUUID() == 0)
return "";
guild = building.getGuild();
return guild == null || guild.isEmptyGuild() ? "" : guild.getName();
}
private static void addDifference(String differenceClass, String line) {
differences.computeIfAbsent(differenceClass, k -> new ArrayList<>()).add(line);
}
}
@@ -0,0 +1,87 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.gameManager.StaticBuildingLoader.StaticBuildingEntry;
import engine.mbEnums.GameObjectType;
import engine.mbEnums.ZonePropType;
import engine.objects.City;
import engine.objects.Guild;
import engine.objects.Zone;
import engine.parsers.objects.GuildSeedEntry;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.HashSet;
// 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. Later zones with the same guild (Junktown under Swampstone) are not cities.
// City IDs follow all static buildings. Never renumber.
public enum StaticCityLoader {
STATICCITYLOADER;
// addToWorld false (parity) keeps the cities out of the cache and the guilds; the static IDs are still taken.
public static ArrayList<StaticCityEntry> buildStaticCities(ArrayList<StaticBuildingEntry> topLevelEntries, boolean addToWorld) {
ArrayList<StaticCityEntry> cityEntries = new ArrayList<>();
HashSet<Guild> guildsWithCity = new HashSet<>();
StaticCityEntry cityEntry;
GuildSeedEntry seedEntry;
Guild guild;
Zone zone;
for (StaticBuildingEntry buildingEntry : topLevelEntries) {
if (buildingEntry.prop.prop_type != ZonePropType.CITY)
continue;
guild = GuildManager._npcGuildsByKey.get(buildingEntry.prop.city_prop_gm_guild);
if (guild == null || !guildsWithCity.add(guild))
continue;
zone = ZoneManager.staticZonesByTreeUID.get(buildingEntry.zoneTreeUID);
seedEntry = GuildManager._npcSeedEntriesByGuildID.get(guild.getObjectUUID());
cityEntry = new StaticCityEntry();
cityEntry.key = "city:" + buildingEntry.key.substring("prop:".length());
cityEntry.staticID = DbManager.nextStaticID(GameObjectType.City, cityEntry.key);
cityEntry.guild = guild;
cityEntry.treeEntry = buildingEntry;
cityEntry.city = City.createStaticCity(addToWorld ? cityEntry.staticID : 0, zone, buildingEntry.building, seedEntry.noobIsle);
cityEntries.add(cityEntry);
if (addToWorld) {
DbManager.addToCache(cityEntry.city);
guild.setCityUUID(cityEntry.staticID);
cityEntry.city.runAfterLoad();
}
}
if (guildsWithCity.size() < GuildManager._npcSeedEntriesByGuildID.size())
Logger.warn("Static cities: " + (GuildManager._npcSeedEntriesByGuildID.size() - guildsWithCity.size())
+ " seed guilds have no CITY prop on this map");
Logger.info("Static cities: " + cityEntries.size() + " built");
return cityEntries;
}
public static class StaticCityEntry {
public String key;
public int staticID;
public Guild guild;
public StaticBuildingEntry treeEntry;
public City city;
}
}
@@ -0,0 +1,157 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.gameManager.StaticCityLoader.StaticCityEntry;
import engine.mbEnums.GameObjectType;
import engine.objects.AbstractGameObject;
import engine.objects.Building;
import engine.objects.City;
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.Objects;
// World Boot Plan 2.5: while obj_city still holds the NPC cities, compares them with the JSON cities (matched by
// parent zone) and writes maps/<map>.city_migration.sql, the old UID -> static ID mapping that
// sql/task25_static_cities.sql re-points obj_guild.ownedCity with. The JSON cities are discarded.
public enum StaticCityParity {
STATICCITYPARITY;
private static final String mapPath = ConfigManager.DEFAULT_DATA_DIR + "maps/";
// Run after NPCs load, so a tree's owner resolves to its guild.
public static void run(int mapID, ArrayList<StaticCityEntry> cityEntries) {
HashMap<Integer, StaticCityEntry> entriesByZone = new HashMap<>();
HashMap<Integer, StaticCityEntry> entriesByDbUID = new HashMap<>();
ArrayList<City> dbCities = new ArrayList<>();
ArrayList<String> differences = new ArrayList<>();
StaticCityEntry entry;
City dbCity;
int jsonOnly = 0;
for (StaticCityEntry cityEntry : cityEntries)
entriesByZone.put(cityEntry.city.parentZoneUUID, cityEntry);
for (AbstractGameObject gameObject : DbManager.getMap(GameObjectType.City).values()) {
dbCity = (City) gameObject;
if (dbCity.isNpc())
dbCities.add(dbCity);
}
for (City city : dbCities) {
entry = entriesByZone.get(city.parentZoneUUID);
if (entry == null) {
differences.add("DB only: " + city.getObjectUUID() + " " + city.getCityName() + " zone " + city.parentZoneUUID);
continue;
}
entriesByDbUID.put(city.getObjectUUID(), entry);
compareCity(city, entry, differences);
}
for (StaticCityEntry cityEntry : cityEntries)
if (!entriesByDbUID.containsValue(cityEntry)) {
differences.add("JSON only: " + cityEntry.key + " " + cityEntry.city.getCityName());
jsonOnly++;
}
Logger.info("Static city parity: " + dbCities.size() + " DB NPC cities, " + cityEntries.size() + " JSON cities, "
+ entriesByDbUID.size() + " matched, " + jsonOnly + " JSON only, " + differences.size() + " differences");
for (String difference : differences)
Logger.info(" " + difference);
writeMigration(mapID, dbCities, entriesByDbUID);
}
private static void compareCity(City dbCity, StaticCityEntry entry, ArrayList<String> differences) {
City jsonCity = entry.city;
Building dbTree = dbCity.getTOL();
String label = dbCity.getObjectUUID() + " " + dbCity.getCityName() + " (" + entry.key + ")";
compare(differences, label, "name", dbCity.getCityName(), jsonCity.getCityName());
compare(differences, label, "tree", describeTree(dbTree), describeTree(jsonCity.getTOL()));
compare(differences, label, "rank", dbCity.getRank(), jsonCity.getRank());
compare(differences, label, "guild", dbCity.getGuild().getName(), entry.guild.getName());
compare(differences, label, "noob isle", dbCity.isNoobIsle, jsonCity.isNoobIsle);
compare(differences, label, "safehold", dbCity.isSafeHold(), jsonCity.isSafeHold());
compare(differences, label, "established", dbCity.established, jsonCity.established);
compare(differences, label, "sieges withstood", dbCity.getSiegesWithstood(), jsonCity.getSiegesWithstood());
compare(differences, label, "open", dbCity.isOpen(), jsonCity.isOpen());
compare(differences, label, "force rename", dbCity.isForceRename(), jsonCity.isForceRename());
compare(differences, label, "bind x/z", dbCity.getBindX() + "/" + dbCity.getBindZ(), jsonCity.getBindX() + "/" + jsonCity.getBindZ());
compare(differences, label, "bind radius", dbCity.getBindRadius() + " type " + dbCity.getRadiusType(),
jsonCity.getBindRadius() + " type " + jsonCity.getRadiusType());
}
private static String describeTree(Building tree) {
if (tree == null)
return "none";
return tree.getObjectUUID() + " mesh " + tree.meshUUID;
}
private static void compare(ArrayList<String> differences, String label, String field, Object dbValue, Object jsonValue) {
if (!Objects.equals(dbValue, jsonValue))
differences.add(label + " " + field + ": DB " + dbValue + ", JSON " + jsonValue);
}
// task25_city_map: every DB NPC city (UID -> static ID, or NULL when unmatched).
private static void writeMigration(int mapID, ArrayList<City> dbCities, HashMap<Integer, StaticCityEntry> entriesByDbUID) {
File migrationFile = new File(mapPath + mapID + ".city_migration.sql");
StringBuilder sql = new StringBuilder();
StaticCityEntry entry;
sql.append("-- World Boot Plan 2.5: old NPC city UID -> static ID for map ").append(mapID)
.append(", written by a JSON boot while obj_city still holds the NPC cities.\n");
sql.append("DROP TABLE IF EXISTS `task25_city_map`;\n");
sql.append("CREATE TABLE `task25_city_map` (`oldUID` BIGINT UNSIGNED NOT NULL, `newID` INT NULL, ")
.append("`staticKey` VARCHAR(64) NULL, PRIMARY KEY (`oldUID`));\n");
sql.append("INSERT INTO `task25_city_map` VALUES\n");
for (int i = 0; i < dbCities.size(); i++) {
entry = entriesByDbUID.get(dbCities.get(i).getObjectUUID());
if (entry == null)
sql.append("(").append(dbCities.get(i).getObjectUUID()).append(", NULL, NULL)");
else
sql.append("(").append(dbCities.get(i).getObjectUUID()).append(", ").append(entry.staticID)
.append(", '").append(entry.key).append("')");
sql.append(i == dbCities.size() - 1 ? ";\n" : ",\n");
}
try {
Files.write(migrationFile.toPath(), sql.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
Logger.error("Static city migration mapping not written: " + migrationFile.getAbsolutePath() + ": " + e);
return;
}
Logger.info("Static city migration mapping: " + dbCities.size() + " rows written to " + migrationFile.getName());
}
}
+306
View File
@@ -0,0 +1,306 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.gameManager;
import engine.mbEnums.GameObjectType;
import engine.parsers.cache.CacheParserUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
// W2 safety net: every static ID with its natural key, in load order, written to maps/<map>.manifest at boot.
// Diffing against the previous boot's file shows IDs that moved; the orphan check shows DB references they broke.
public enum StaticManifest {
STATICMANIFEST;
private static final String mapPath = ConfigManager.DEFAULT_DATA_DIR + "maps/";
// Player data columns that can hold a static ID (findings §5, Task 2.1). A null type accepts any static object.
private static final String[][] referenceColumns = {
{"object", "parent", null},
{"obj_character", "char_bindBuilding", "Building"},
{"obj_character", "guildUID", "Guild"},
{"dyn_character_guildhistory", "GuildID", "Guild"},
{"dyn_guild_allianceenemylist", "OtherGuildID", "Guild"},
{"dyn_guild_guildkoslist", "KOSGuildID", "Guild"},
{"obj_mob", "mob_buildingID", "Building"},
{"obj_mob", "mob_guildUID", "Guild"},
{"obj_npc", "npc_buildingID", "Building"},
{"obj_npc", "npc_guildID", "Guild"},
{"obj_city", "treeOfLifeUUID", "Building"},
{"obj_guild", "ownedCity", "City"},
{"static_runegate_portals", "sourceBuilding", "Building"},
{"static_runegate_portals", "targetBuilding", "Building"},
{"obj_mine", "mine_buildingUID", "Building"}
};
private static final int LOGGED_MOVES = 20;
private static final ArrayList<ManifestEntry> entries = new ArrayList<>();
private static final HashMap<Integer, ManifestEntry> entriesByID = new HashMap<>();
private static final HashMap<String, ManifestEntry> entriesByKey = new HashMap<>();
public static void addEntry(int staticID, GameObjectType objectType, String key) {
ManifestEntry entry = new ManifestEntry(staticID, objectType.name(), key);
if (entriesByKey.containsKey(key))
throw new IllegalStateException("Static manifest key " + key + " is used twice");
entries.add(entry);
entriesByID.put(staticID, entry);
entriesByKey.put(key, entry);
}
// Logs moved, vanished and re-targeted IDs against the previous manifest, then replaces it (old copy kept as .prev).
// Moved IDs leave DB references on the wrong objects: they are written to <map>.renumber.sql and the count is
// returned so boot can stop before play.
public static int writeManifest(int mapID) {
File manifestFile = new File(mapPath + mapID + ".manifest");
File previousFile = new File(mapPath + mapID + ".manifest.prev");
HashMap<String, ManifestEntry> previousByKey;
StringBuilder manifestText = new StringBuilder();
ArrayList<ManifestEntry[]> movedEntries = new ArrayList<>();
previousByKey = readManifest(manifestFile, mapID);
if (previousByKey != null) {
diffManifest(previousByKey, movedEntries);
if (!movedEntries.isEmpty())
writeRenumber(mapID, movedEntries);
}
manifestText.append("{\n \"map\": ").append(mapID).append(",\n \"count\": ").append(entries.size())
.append(",\n \"statics\": [\n");
for (int i = 0; i < entries.size(); i++) {
manifestText.append(" ").append(entries.get(i).toJson());
manifestText.append(i < entries.size() - 1 ? ",\n" : "\n");
}
manifestText.append(" ]\n}\n");
try {
if (manifestFile.exists())
Files.copy(manifestFile.toPath(), previousFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.write(manifestFile.toPath(), manifestText.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
Logger.error("Static manifest not written: " + manifestFile.getAbsolutePath() + ": " + e);
return movedEntries.size();
}
Logger.info("Static manifest: " + entries.size() + " static IDs written to " + manifestFile.getName());
return movedEntries.size();
}
// A renumber file is deleted by hand once its SQL ran; until then the DB still holds the old IDs.
public static boolean isRenumberPending(int mapID) {
return new File(mapPath + mapID + ".renumber.sql").exists();
}
// static_renumber_map: old ID -> new ID per moved key, read by World-Boot-Plan sql/static_renumber.sql.
private static void writeRenumber(int mapID, ArrayList<ManifestEntry[]> movedEntries) {
File renumberFile = new File(mapPath + mapID + ".renumber.sql");
StringBuilder sql = new StringBuilder();
ManifestEntry previousEntry;
ManifestEntry currentEntry;
sql.append("-- Static IDs that moved since the previous manifest of map ").append(mapID).append(".\n");
sql.append("DROP TABLE IF EXISTS `static_renumber_map`;\n");
sql.append("CREATE TABLE `static_renumber_map` (`oldID` INT NOT NULL, `newID` INT NOT NULL, `type` VARCHAR(32) NOT NULL, ")
.append("`staticKey` VARCHAR(64) NOT NULL, PRIMARY KEY (`oldID`));\n");
for (int i = 0; i < movedEntries.size(); i++) {
previousEntry = movedEntries.get(i)[0];
currentEntry = movedEntries.get(i)[1];
if (i % 500 == 0)
sql.append("INSERT INTO `static_renumber_map` VALUES\n");
sql.append("(").append(previousEntry.id).append(", ").append(currentEntry.id).append(", '")
.append(currentEntry.type).append("', '").append(currentEntry.key).append("')");
sql.append(i % 500 == 499 || i == movedEntries.size() - 1 ? ";\n" : ",\n");
}
try {
Files.write(renumberFile.toPath(), sql.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
Logger.error("Static renumber mapping not written: " + renumberFile.getAbsolutePath() + ": " + e);
return;
}
Logger.warn("Static manifest: " + movedEntries.size() + " moved IDs written to " + renumberFile.getName());
}
// Logs every negative DB reference (below -1, which means "none") that no loaded static object of the right type holds.
public static void logOrphanReferences() {
HashMap<Integer, Integer> negativeValues;
ManifestEntry entry;
String expectedType;
String problem;
int orphanColumns = 0;
int orphanRows = 0;
for (String[] referenceColumn : referenceColumns) {
expectedType = referenceColumn[2];
negativeValues = DbManager.StaticWorldQueries.GET_NEGATIVE_REFERENCES(referenceColumn[0], referenceColumn[1]);
if (negativeValues == null)
continue;
for (Map.Entry<Integer, Integer> negativeValue : negativeValues.entrySet()) {
entry = entriesByID.get(negativeValue.getKey());
if (entry == null)
problem = "no static object";
else if (DbManager.getFromCache(GameObjectType.valueOf(entry.type), entry.id) == null)
problem = entry.type + " " + entry.key + " is not loaded";
else if (expectedType != null && !expectedType.equals(entry.type))
problem = "points at " + entry.type + " " + entry.key + ", expected " + expectedType;
else
continue;
Logger.warn("Orphan reference: " + referenceColumn[0] + "." + referenceColumn[1] + " = "
+ negativeValue.getKey() + " on " + negativeValue.getValue() + " rows: " + problem);
orphanColumns++;
orphanRows += negativeValue.getValue();
}
}
Logger.info("Static orphan check: " + orphanColumns + " orphan values on " + orphanRows + " rows");
}
private static HashMap<String, ManifestEntry> readManifest(File manifestFile, int mapID) {
HashMap<String, ManifestEntry> previousByKey = new HashMap<>();
JSONObject manifestJson;
JSONArray staticsJson;
JSONObject entryJson;
if (!manifestFile.exists()) {
Logger.info("Static manifest: no previous " + manifestFile.getName() + ", nothing to diff");
return null;
}
try {
manifestJson = CacheParserUtil.readJsonFile(manifestFile);
if (manifestJson.getInt("map") != mapID) {
Logger.error(manifestFile.getName() + ": map " + manifestJson.getInt("map") + " does not match MB_WORLD_MAPID " + mapID);
return null;
}
staticsJson = manifestJson.getJSONArray("statics");
for (int i = 0; i < staticsJson.length(); i++) {
entryJson = staticsJson.getJSONObject(i);
previousByKey.put(entryJson.getString("key"),
new ManifestEntry(entryJson.getInt("id"), entryJson.getString("type"), entryJson.getString("key")));
}
} catch (Exception e) {
Logger.error("Previous static manifest unreadable, nothing to diff: " + manifestFile.getName() + ": " + e);
return null;
}
return previousByKey;
}
// A re-targeted ID is the dangerous case: player references to it now silently point at a different object.
private static void diffManifest(HashMap<String, ManifestEntry> previousByKey, ArrayList<ManifestEntry[]> movedEntries) {
HashMap<Integer, ManifestEntry> previousByID = new HashMap<>();
ManifestEntry currentEntry;
ManifestEntry previousEntry;
int moved = 0;
int vanished = 0;
int retargeted = 0;
int added = 0;
for (ManifestEntry entry : previousByKey.values())
previousByID.put(entry.id, entry);
for (ManifestEntry entry : previousByKey.values()) {
currentEntry = entriesByKey.get(entry.key);
if (currentEntry == null) {
Logger.warn("Static manifest: " + entry.type + " " + entry.key + " (was " + entry.id + ") vanished");
vanished++;
} else if (currentEntry.id != entry.id) {
if (moved < LOGGED_MOVES)
Logger.warn("Static manifest: " + entry.type + " " + entry.key + " moved " + entry.id + " -> " + currentEntry.id);
movedEntries.add(new ManifestEntry[]{entry, currentEntry});
moved++;
}
}
for (ManifestEntry entry : entries) {
previousEntry = previousByID.get(entry.id);
if (!previousByKey.containsKey(entry.key))
added++;
if (previousEntry != null && !previousEntry.key.equals(entry.key)) {
if (retargeted < LOGGED_MOVES)
Logger.warn("Static manifest: ID " + entry.id + " re-targeted from " + previousEntry.type + " "
+ previousEntry.key + " to " + entry.type + " " + entry.key);
retargeted++;
}
}
Logger.info("Static manifest diff: " + moved + " moved, " + vanished + " vanished, " + retargeted
+ " re-targeted, " + added + " new");
}
private static class ManifestEntry {
private final int id;
private final String type;
private final String key;
private ManifestEntry(int id, String type, String key) {
this.id = id;
this.type = type;
this.key = key;
}
private String toJson() {
return "{\"id\": " + id + ", \"type\": " + JSONObject.quote(type) + ", \"key\": " + JSONObject.quote(key) + "}";
}
}
}
+132 -2
View File
@@ -17,7 +17,10 @@ import engine.mbEnums;
import engine.objects.Building;
import engine.objects.City;
import engine.objects.Zone;
import engine.objects.ZoneTemplate;
import engine.parsers.map.ZoneTreeParser;
import engine.parsers.objects.CZoneProp;
import engine.parsers.objects.CZoneTemplate;
import engine.parsers.objects.ZoneTreeEntry;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
@@ -36,7 +39,11 @@ public enum ZoneManager {
ZONEMANAGER;
public static HashMap<Integer, ZoneTemplate> _zone_templates = new HashMap<>();
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 ArrayList<Zone> ruinZones = new ArrayList<>();
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);
@@ -51,6 +58,89 @@ public enum ZoneManager {
/* Instance variables */
public static Zone seaFloor = null;
// CZONE templates plus the server's player-city template 0, which has no CZONE file
public static void loadZoneTemplates() {
CZoneTemplate playerCityTemplate = new CZoneTemplate();
_zone_templates.putAll(CZoneTemplate._zones);
playerCityTemplate.template_id = 0;
playerCityTemplate.zone_sea_level_type = mbEnums.SeaLevelType.PARENT;
playerCityTemplate.zone_sea_level = 0;
playerCityTemplate.zone_max_blend = 128;
playerCityTemplate.zone_min_blend = 128;
playerCityTemplate.terrain_max_y = 5;
playerCityTemplate.zone_major_radius = (int) mbEnums.CityBoundsType.ZONE.halfExtents;
playerCityTemplate.zone_minor_radius = (int) mbEnums.CityBoundsType.ZONE.halfExtents;
playerCityTemplate.terrain_type = mbEnums.TerrainGenType.PLANAR;
_zone_templates.put(playerCityTemplate.template_id, playerCityTemplate);
Logger.info("Zone templates: " + _zone_templates.size());
}
// Read before the zone templates, so only the templates this map uses are parsed. False aborts boot.
public static boolean loadZoneTree(int mapID) {
zoneTree = ZoneTreeParser.parseZoneTree(mapID);
return zoneTree != null;
}
public static TreeSet<Integer> getZoneTreeTemplateIDs() {
TreeSet<Integer> templateIDs = new TreeSet<>();
for (ZoneTreeEntry treeEntry : zoneTree)
templateIDs.add(treeEntry.template);
return templateIDs;
}
// 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(boolean useStaticIDs) {
ArrayList<Zone> cityZones;
Zone zone;
int staticID;
int parentZoneID;
int staticCount = 0;
for (ZoneTreeEntry treeEntry : zoneTree) {
if (treeEntry.parent != 0 && !staticZonesByTreeUID.containsKey(treeEntry.parent)) {
Logger.error("Zone " + treeEntry.uid + " (" + treeEntry.name + ") has no parent zone " + treeEntry.parent);
return false;
}
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++;
}
cityZones = DbManager.ZoneQueries.GET_PLAYER_CITY_ZONES();
Logger.info("Zones: " + staticCount + " static, " + cityZones.size() + " player city");
return true;
}
// Find all zones coordinates fit into, starting with Sea Floor
public static ArrayList<Zone> getAllZonesIn(final Vector3fImmutable loc) {
@@ -139,6 +229,46 @@ public enum ZoneManager {
return ZoneManager.zonesByName.get(zoneName);
}
// Ruins: static zones whose top-level CITY props carry a seed ruin key (the 8 Ruins on map 108). Needs the guild seed.
public static void loadRuinZones() {
Zone zone;
for (ZoneTreeEntry treeEntry : ZoneManager.zoneTree) {
zone = ZoneManager.staticZonesByTreeUID.get(treeEntry.uid);
for (CZoneProp prop : zone.template.zone_prop_info)
if (prop.prop_type == mbEnums.ZonePropType.CITY && GuildManager._ruinKeys.contains(prop.city_prop_gm_guild)) {
ZoneManager.ruinZones.add(zone);
break;
}
}
Logger.info("Ruins: " + ZoneManager.ruinZones.size() + " zones" + (ConfigManager.MB_USE_RUINS.getValue().equalsIgnoreCase("true")
? "" : " (MB_USE_RUINS off: respawn at Sea Dog's Rest)"));
}
// Respawn point of a character with no usable bind: a random Ruin, or Sea Dog's Rest when ruins are off or missing.
public static Vector3fImmutable getRandomRuinLocation() {
Zone ruinZone;
if (ConfigManager.MB_USE_RUINS.getValue().equalsIgnoreCase("true") && !ZoneManager.ruinZones.isEmpty()) {
ruinZone = ZoneManager.ruinZones.get(ThreadLocalRandom.current().nextInt(ZoneManager.ruinZones.size()));
return Vector3fImmutable.getRandomPointOnCircle(ruinZone.getLoc(), 30);
}
ruinZone = ZoneManager.getZoneByName("sea dog's rest");
// 14001 does not have a banestone to bind at
if (ruinZone.templateID == 14001)
return Vector3fImmutable.getRandomPointOnCircle(ruinZone.getLoc(), 30);
return Vector3fImmutable.getRandomPointOnCircle(ruinZone.getLoc().add(new Vector3fImmutable(-196.016f, 2.812f, 203.621f)), 30);
}
public static Collection<Zone> getAllZones() {
return ZoneManager.zonesByUUID.values();
}
+3 -3
View File
@@ -408,7 +408,7 @@ public class Bounds {
blueprint = building.getBlueprint();
this.quaternion = new Quaternion(building.getRot().x, building.getRot().y, building.getRot().z, building.getw());
this.quaternion = new Quaternion(building.getRotation().x, building.getRotation().y, building.getRotation().z, building.getRotation().w);
// Calculate Bounds for non-blueprint objects
@@ -437,7 +437,7 @@ public class Bounds {
// The rotation is reset after the new aabb is calculated.
this.rotation = building.getRot().y;
this.rotation = building.getRotation().y;
// Caclculate and set the new half halfExtents for the rotated bounding box
// and reset the rotation to 0 for this bounds.
@@ -451,7 +451,7 @@ public class Bounds {
}
this.origin.set(building.getLoc().x, building.getLoc().z);
this.rotation = building.getRot().y;
this.rotation = building.getRotation().y;
this.halfExtents.set(blueprint.getExtents());
this.flipExtents = Bounds.calculateFlipExtents(this);
+11
View File
@@ -1142,6 +1142,17 @@ public class Quaternion {
return opposite(this);
}
// Client MakeRotateY (Math.dll 0x10002960): w = cos θ/2, y = sin θ/2, radians.
// Math rather than FastMath: FastMath.reduceSinAngle mis-reduces angles below -π.
public static Quaternion fromYaw(float yaw) {
return new Quaternion(0f, (float) Math.sin(yaw * 0.5), 0f, (float) Math.cos(yaw * 0.5));
}
// The angle MakeRotateY would need to produce this rotation; characters send it as one float.
public float getYaw() {
return 2 * FastMath.atan2(y, w);
}
@Override
public Quaternion clone() {
try {
+45
View File
@@ -0,0 +1,45 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.math;
// The client's placement (ArcObjectInfo +0x28/+0x34/+0x44, math::Transformation); see MBDATA/specs/WorldPlacement.md §7.
public class Transform {
public final Vector3fImmutable location;
public final Quaternion rotation;
public final Vector3f scale;
public Transform(Vector3fImmutable location, Quaternion rotation, Vector3f scale) {
this.location = location;
this.rotation = rotation;
this.scale = scale;
}
public static Transform fromYaw(Vector3fImmutable location, float yaw) {
return new Transform(location, Quaternion.fromYaw(yaw), new Vector3f(1.0f, 1.0f, 1.0f));
}
// world = parent × local, as the client composes zone and object transforms (sb.exe 0x6516C0).
public static Transform mult(Transform parent, Transform local) {
Vector3f scaledLocation;
Vector3f rotatedLocation;
Quaternion rotation;
Vector3f scale;
scaledLocation = new Vector3f(local.location).mult(parent.scale);
rotatedLocation = parent.rotation.mult(scaledLocation);
rotation = parent.rotation.mult(local.rotation);
scale = parent.scale.mult(local.scale);
return new Transform(parent.location.add(rotatedLocation), rotation, scale);
}
}
+52 -60
View File
@@ -8,15 +8,11 @@
package engine;
import engine.gameManager.ConfigManager;
import engine.gameManager.PowersManager;
import engine.gameManager.ZoneManager;
import engine.math.Vector2f;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.objects.AbstractCharacter;
import engine.objects.Shrine;
import engine.objects.Zone;
import engine.powers.EffectsBase;
import engine.powers.RunePowerEntry;
import org.pmw.tinylog.Logger;
@@ -25,7 +21,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.concurrent.ThreadLocalRandom;
/*
* MagicBane engine enumeration class.
@@ -504,61 +499,6 @@ public class mbEnums {
}
public enum Ruins {
ESTRAGOTH(569),
KARFELL(570),
MORELAN(571),
REGARS(572),
HALLOS(573),
WESTERMORE(574),
EYWAN(575),
CAER(576);
private final int zoneUUID;
Ruins(int uuid) {
this.zoneUUID = uuid;
}
public static Ruins getRandomRuin() {
Ruins ruins;
ruins = Ruins.values()[ThreadLocalRandom.current()
.nextInt(Ruins.values().length)];
return ruins;
}
public Vector3fImmutable getLocation() {
Zone ruinZone;
Vector3fImmutable spawnLocation;
// Send to SDR if so configured
if (ConfigManager.MB_USE_RUINS.getValue().equalsIgnoreCase("true")) {
ruinZone = ZoneManager.getZoneByUUID(this.zoneUUID);
spawnLocation = Vector3fImmutable.getRandomPointOnCircle(ruinZone.getLoc(), 30);
} else {
ruinZone = ZoneManager.getZoneByName("sea dog's rest");
// 14001 does not have a banestone to bind at
if (ruinZone.templateID == 14001)
spawnLocation = Vector3fImmutable.getRandomPointOnCircle(ruinZone.getLoc(), 30);
else
spawnLocation = Vector3fImmutable.getRandomPointOnCircle(ruinZone.getLoc()
.add(new Vector3fImmutable(-196.016f, 2.812f, 203.621f)), 30);
}
return spawnLocation;
}
}
public enum Guards {
HumanArcher(13.97f, 13.97f, 6.19f, 4.2199998f, 18.38f, 6.33f, 6.5f),
@@ -3147,6 +3087,58 @@ public class mbEnums {
MULTIPLY
}
// CZONE value sets; only the values present in the shipped cache.
public enum ZoneShapeType {
ELLIPTICAL,
RECTANGULAR
}
public enum SeaLevelType {
PARENT,
SELF,
WORLD
}
// NONE is the server's marker for a zone with no zone_terrain_gen block
public enum TerrainGenType {
NONE,
TARGA,
PLANAR
}
public enum ZonePropType {
PROP,
CITY
}
public enum ZoneMobileType {
MOBILE,
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
}
public enum ServerConfig {
MBDB_108("AERYNTH", "ARAC", "MOURNING", 300000),
MBDB_192("DALGOTH", "ARAC", "GOOK", 300001),
@@ -62,6 +62,12 @@ public class ItemProductionMsgHandler extends AbstractClientMsgHandler {
if (vendor == null)
return true;
// Same access rule as opening the vendor's management window (OrderNPCMsgHandler); never trust the client
if (!BuildingManager.PlayerCanControlNotOwner(vendor.getBuilding(), player)
&& !BuildingManager.playerCanManage(player, vendor.getBuilding()))
return true;
// Process Request
switch (msg.actionType) {
@@ -299,6 +305,11 @@ public class ItemProductionMsgHandler extends AbstractClientMsgHandler {
if (vendor.getBuilding() == null)
return;
// NPC buildings have no strongbox to refund into
if (vendor.getBuilding().getProtectionState() == mbEnums.ProtectionState.NPC)
return;
PlayerCharacter player = origin.getPlayerCharacter();
if (player == null)
@@ -1180,10 +1180,10 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
// if W return is negative, this is a -90 rotation not a 90?
newMesh = DbManager.BuildingQueries.CREATE_BUILDING(
currentZone.getObjectUUID(), playerCharacter.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
newMesh = Building.persistBuilding(Building.createBuilding(
currentZone, playerCharacter.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
localLoc, 1.0f, blueprint.getMaxHealth(0), ProtectionState.NONE, 0, 0,
completionDate, blueprint.getMeshForRank(0), vendorRotation, buildingRotation);
completionDate, blueprint.getMeshForRank(0), vendorRotation, buildingRotation));
// Make sure we have a valid mesh
@@ -1303,10 +1303,10 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
DateTime completionDate = DateTime.now().plusHours(blueprint.getRankTime(1));
newMesh = DbManager.BuildingQueries.CREATE_BUILDING(
currentZone.getObjectUUID(), player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
newMesh = Building.persistBuilding(Building.createBuilding(
currentZone, player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
localLoc, 1.0f, blueprint.getMaxHealth(0), ProtectionState.PROTECTED, 0, 0,
completionDate, blueprint.getMeshForRank(0), vendorRotation, buildingRotation);
completionDate, blueprint.getMeshForRank(0), vendorRotation, buildingRotation));
// Make sure we have a valid mesh
if (newMesh == null) {
@@ -1342,10 +1342,10 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
City city = City.getCity(currentZone.playerCityUUID);
warehouseBuilding = DbManager.BuildingQueries.CREATE_BUILDING(
currentZone.getObjectUUID(), player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
warehouseBuilding = Building.persistBuilding(Building.createBuilding(
currentZone, player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
localLoc, 1.0f, blueprint.getMaxHealth(0), ProtectionState.NONE, 0, 0,
DateTime.now().plusHours(blueprint.getRankTime(1)), blueprint.getMeshForRank(0), vendorRotation, buildingRotation);
DateTime.now().plusHours(blueprint.getRankTime(1)), blueprint.getMeshForRank(0), vendorRotation, buildingRotation));
if (warehouseBuilding == null) {
PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity");
@@ -1363,7 +1363,6 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
MaintenanceManager.setMaintDateTime(warehouseBuilding, LocalDateTime.now().plusDays(7));
warehouseBuilding.setLoc(warehouseBuilding.getLoc());
InterestManager.setObjectDirty(warehouseBuilding);
warehouseBuilding.runAfterLoad();
if (city == null)
return true;
+13
View File
@@ -21,6 +21,7 @@ import engine.jobs.PersistentAoeJob;
import engine.jobs.TrackJob;
import engine.math.AtomicFloat;
import engine.math.Bounds;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
@@ -536,6 +537,18 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
}
// The one yaw float LOADCHARACTER carries; a character in a building gets the building's yaw added.
public float getClientYaw() {
float yaw = this.getRotation().getYaw();
if (this.building != null)
if (this.building.getBounds() != null && this.building.getBounds().getQuaternion() != null)
yaw += this.building.getBounds().getQuaternion().angleY;
return yaw;
}
public static void teleport(AbstractCharacter abstractCharacter, final Vector3fImmutable targetLoc) {
abstractCharacter.locationLock.writeLock().lock();
try {
+18 -8
View File
@@ -18,7 +18,7 @@ import engine.job.JobScheduler;
import engine.jobs.NoTimeJob;
import engine.math.AtomicFloat;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums.DispatchChannel;
import engine.mbEnums.EffectSourceType;
@@ -56,9 +56,11 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
protected boolean movingUp = false;
private String name = "";
private byte tier = 0;
private Vector3f rot = new Vector3f(0.0f, 0.0f, 0.0f);
private Quaternion rotation = new Quaternion();
private int objectTypeMask = 0;
private Bounds bounds;
// Built from world data with an ID from DbManager.nextStaticID(); never written to the DB.
private boolean staticWorldObject = false;
/**
* No Id Constructor
@@ -528,13 +530,13 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
}
public Vector3f getRot() {
return rot;
public Quaternion getRotation() {
return rotation;
}
public void setRot(Vector3f rotation) {
synchronized (this.rot) {
this.rot = rotation;
public void setRotation(Quaternion rotation) {
synchronized (this.rotation) {
this.rotation = rotation;
}
}
@@ -543,7 +545,7 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
}
public void setTier(byte tier) {
synchronized (this.rot) {
synchronized (this.rotation) {
this.tier = tier;
}
}
@@ -594,6 +596,14 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
this.bounds = bounds;
}
public boolean isStaticWorldObject() {
return this.staticWorldObject;
}
public void setStaticWorldObject(boolean staticWorldObject) {
this.staticWorldObject = staticWorldObject;
}
public float getAltitude() {
return altitude;
}
+3 -3
View File
@@ -210,10 +210,10 @@ public final class Bane {
return false;
}
Building stone = DbManager.BuildingQueries.CREATE_BUILDING(
cityZone.getObjectUUID(), player.getObjectUUID(), blueprint.getName(), blueprint.getBlueprintUUID(),
Building stone = Building.persistBuilding(Building.createBuilding(
cityZone, player.getObjectUUID(), blueprint.getName(), blueprint.getBlueprintUUID(),
localLocation, 1.0f, blueprint.getMaxHealth(rank), ProtectionState.PROTECTED, 0, rank,
null, blueprint.getBlueprintUUID(), 1, 0.0f);
null, blueprint.getBlueprintUUID(), 1, 0.0f));
if (stone == null) {
PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity");
+161 -24
View File
@@ -24,6 +24,8 @@ import engine.job.JobScheduler;
import engine.jobs.DoorCloseJob;
import engine.jobs.SiegeSpireWithdrawlJob;
import engine.math.Bounds;
import engine.math.Quaternion;
import engine.math.Transform;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
@@ -31,11 +33,15 @@ import engine.mbEnums.*;
import engine.net.ByteBufferWriter;
import engine.net.client.msg.ApplyBuildingEffectMsg;
import engine.net.client.msg.UpdateObjectMsg;
import engine.parsers.objects.CZonePlacement;
import engine.parsers.objects.CZoneProp;
import engine.server.MBServerStatics;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
@@ -83,7 +89,6 @@ public class Building extends AbstractWorldObject {
public ArrayList<Vector3fImmutable> patrolPoints;
public ProtectionState protectionState = ProtectionState.NONE;
protected Resists resists;
private float w = 1.0f;
private Vector3f meshScale = new Vector3f(1.0f, 1.0f, 1.0f);
private int doorState = 0;
private int ownerUUID = 0; //NPC or Character--check ownerIsNPC flag
@@ -99,6 +104,14 @@ public class Building extends AbstractWorldObject {
private ConcurrentHashMap<Integer, Condemned> condemned;
private ArrayList<Building> children = null;
/**
* No Id Constructor
*/
public Building() {
super();
}
/**
* ResultSet Constructor
*/
@@ -118,8 +131,7 @@ public class Building extends AbstractWorldObject {
this.ownerUUID = rs.getInt("ownerUUID");
this.doorState = rs.getInt("doorState");
this.setHealth(rs.getInt("currentHP"));
this.w = rs.getFloat("w");
this.setRot(new Vector3f(0f, rs.getFloat("rotY"), 0f));
this.setRotation(new Quaternion(0f, rs.getFloat("rotY"), 0f, rs.getFloat("w")));
this.reverseKOS = rs.getByte("reverseKOS") == 1;
this.statLat = rs.getFloat("locationX");
this.statAlt = rs.getFloat("locationY");
@@ -173,6 +185,122 @@ public class Building extends AbstractWorldObject {
}
}
// Defaults mirror the obj_building column defaults so a new building matches a reloaded one.
public static Building createBuilding(Zone parentZone, int ownerUUID, String name, int meshUUID,
Vector3fImmutable localLoc, float scale, int currentHP,
ProtectionState protectionState, int currentGold, int rank,
DateTime upgradeDate, int blueprintUUID, float w, float rotY) {
Building building = new Building();
building.setObjectTypeMask(MBServerStatics.MASK_BUILDING);
building.gridObjectType = GridObjectType.STATIC;
building.parentZoneUUID = parentZone.getObjectUUID();
building.ownerUUID = ownerUUID;
building.name = name;
building.meshUUID = meshUUID;
building.blueprintUUID = blueprintUUID;
building.statLat = localLoc.x;
building.statAlt = localLoc.y;
building.statLon = localLoc.z;
building.meshScale = new Vector3f(scale, scale, scale);
building.setHealth(currentHP);
building.protectionState = protectionState;
building._strongboxValue = currentGold;
building.maxGold = 15000000;
building.rank = rank;
building.setRotation(new Quaternion(0f, rotY, 0f, w));
building.parentBuildingID = 0;
building.floor = -1;
building.level = -1;
if (upgradeDate != null)
building.upgradeDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(upgradeDate.getMillis()), ZoneId.systemDefault());
return building;
}
// 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;
building.parentZoneUUID = parentZone.getObjectUUID();
building.name = placement.basic_zone_name_override;
building.meshUUID = placement.basic_zone_template_id;
building.statLat = localLoc.x;
building.statAlt = parentBuilding == null ? 0 : localLoc.y;
building.statLon = localLoc.z;
building.meshScale = new Vector3f(1.0f, 1.0f, 1.0f);
// CZONE y_rot is not the client's yaw: live servers sent π − y_rot (specs/WorldPlacement.md §3, W8).
building.setRotation(Quaternion.fromYaw((float) (Math.PI - placement.basic_zone_y_rot)));
building.protectionState = ProtectionState.NPC;
building.maxGold = 15000000;
building.ownerUUID = ownerUUID;
building.ownerIsNPC = ownerUUID != 0;
if (prop.prop_type == ZonePropType.CITY) {
building.rank = prop.city_prop_asset_rank;
building._strongboxValue = prop.city_prop_asset_funds;
}
// Same swap as the DB rows; runAfterLoad's region lookup expects it.
building.floor = placement.basic_zone_level_number;
building.level = placement.basic_zone_room_number;
building.isFurniture = parentBuilding != null;
building.parentBuildingID = parentBuilding == null ? 0 : parentBuilding.getObjectUUID();
building.setLoc(getStaticWorldLocation(building, parentZone, parentBuilding));
return building;
}
// Top-level: zone position on the terrain. Child: parent × local, so the parent's yaw turns the offset.
private static Vector3fImmutable getStaticWorldLocation(Building building, Zone parentZone, Building parentBuilding) {
Vector3fImmutable groundLoc;
Transform parentTransform;
Transform localTransform;
if (parentBuilding == null) {
groundLoc = new Vector3fImmutable(building.statLat + parentZone.absX, 0, building.statLon + parentZone.absZ);
return groundLoc.setY(Terrain.getWorldHeight(groundLoc) + building.statAlt);
}
parentTransform = new Transform(parentBuilding.getLoc(), parentBuilding.getRotation(), parentBuilding.meshScale);
localTransform = new Transform(new Vector3fImmutable(building.statLat, building.statAlt, building.statLon),
building.getRotation(), building.meshScale);
return Transform.mult(parentTransform, localTransform).location;
}
// Writes a new player building once and loads it into the simulation; returns null if the insert failed.
public static Building persistBuilding(Building building) {
int objectUUID;
objectUUID = DbManager.BuildingQueries.PERSIST(building);
if (objectUUID <= 0)
return null;
building.setObjectUUID(objectUUID);
DbManager.addToCache(building);
building.runAfterLoad();
return building;
}
/*
* Getters
*/
@@ -198,11 +326,11 @@ public class Building extends AbstractWorldObject {
writer.putFloat(building.getLoc().getZ());
}
writer.putFloat(building.w);
writer.putFloat(0f);
writer.putFloat(building.getRot().y);
writer.putFloat(building.getRotation().w);
writer.putFloat(building.getRotation().x);
writer.putFloat(building.getRotation().y);
writer.putFloat(building.getRotation().z);
writer.putFloat(0f);
writer.putFloat(building.meshScale.getX());
writer.putFloat(building.meshScale.getY());
writer.putFloat(building.meshScale.getZ());
@@ -222,6 +350,24 @@ public class Building extends AbstractWorldObject {
writer.put((byte) 0); // 0
writer.putFloat(3); // 3
// Live static props and their furniture: no own ID, no owner, flag clear, and no health on furniture.
if (building.isStaticWorldObject()) {
for (int i = 0; i < 4; i++)
writer.putInt(0);
writer.put((byte) 0);
if (building.parentBuildingID != 0) {
writer.putFloat(0);
writer.putFloat(0);
} else {
writer.putFloat(building.health.get());
writer.putFloat(building.healthMax);
}
} else {
writer.putInt(GameObjectType.Building.ordinal());
writer.putInt(building.getObjectUUID());
@@ -235,6 +381,7 @@ public class Building extends AbstractWorldObject {
writer.put((byte) 1); // End Datablock
writer.putFloat(building.health.get());
writer.putFloat(building.healthMax);
}
if (building.blueprintUUID == 0)
writer.putInt(0);
@@ -689,14 +836,6 @@ public class Building extends AbstractWorldObject {
this.healthMax = maxHealth;
}
public final void setw(float value) {
this.w = value;
}
public final float getw() {
return this.w;
}
public final Vector3f getMeshScale() {
return this.meshScale;
}
@@ -945,7 +1084,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) {
@@ -964,15 +1105,11 @@ public class Building extends AbstractWorldObject {
}
}
// Submit upgrade job if building is currently set to rank.
// City load reads ownerIsNPC before NPCs are cached, so boot uses the owner set read ahead of buildings.
try {
DbObjectType objectType = DbManager.BuildingQueries.GET_UID_ENUM(this.ownerUUID);
this.ownerIsNPC = (objectType == DbObjectType.NPC);
} catch (Exception e) {
this.ownerIsNPC = false;
Logger.error("Failed to find Object Type for owner " + this.ownerUUID + " Location " + this.getLoc().toString());
}
if (!this.isStaticWorldObject())
this.ownerIsNPC = BuildingManager._npcBuildingOwners.contains(this.ownerUUID)
|| DbManager.inCache(GameObjectType.NPC, this.ownerUUID);
// Reference friend and condemn lists from BuildingManager
+62 -1
View File
@@ -141,6 +141,43 @@ public class City extends AbstractWorldObject {
}
public City() {
super();
}
// NPC city of a seed guild (World Boot Plan 2.5). Fields the JSON lacks take the values every DB NPC city row had.
public static City createStaticCity(int cityID, Zone parentZone, Building treeOfLife, boolean noobIsle) {
City city = new City();
city.setObjectUUID(cityID);
city.setStaticWorldObject(true);
city.gridObjectType = GridObjectType.STATIC;
city.parentZoneUUID = parentZone.getObjectUUID();
city.cityName = parentZone.zoneName;
city.motto = "";
city.description = "";
city.isNpc = 1;
city.isSafeHold = 1;
city.isNoobIsle = noobIsle ? 1 : 0;
city.established = LocalDateTime.of(2003, 3, 25, 9, 0);
city.realmTaxDate = LocalDateTime.now();
city.treeOfLifeID = treeOfLife.getObjectUUID();
city.bindX = 0;
city.bindZ = 0;
city.bindLoc = Vector3fImmutable.ZERO;
city.radiusType = 1;
// DB rows held 32, and the ResultSet constructor takes 2 off
city.bindRadius = 30;
city.forceRename = false;
city.open = false;
city.hash = DataWarehouse.hasher.encryptObjectID(cityID);
return city;
}
/*
* Utils
*/
@@ -281,7 +318,7 @@ public class City extends AbstractWorldObject {
city = City.getCity(cityID);
if (city == null)
return mbEnums.Ruins.getRandomRuin().getLocation();
return ZoneManager.getRandomRuinLocation();
return city.getBindLoc();
}
@@ -1279,6 +1316,30 @@ public class City extends AbstractWorldObject {
return forceRename;
}
public int getTreeOfLifeID() {
return treeOfLifeID;
}
public boolean isNpc() {
return isNpc == 1;
}
public float getBindX() {
return bindX;
}
public float getBindZ() {
return bindZ;
}
public float getBindRadius() {
return bindRadius;
}
public int getRadiusType() {
return radiusType;
}
public void setForceRename(boolean forceRename) {
if (!DbManager.CityQueries.updateforceRename(this, forceRename))
return;
+44 -9
View File
@@ -21,6 +21,7 @@ import engine.net.client.msg.AllianceChangeMsg;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.UpdateClientAlliancesMsg;
import engine.net.client.msg.guild.GuildInfoMsg;
import engine.parsers.objects.GuildSeedEntry;
import engine.server.world.WorldServer;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;
@@ -35,15 +36,15 @@ public class Guild extends AbstractWorldObject {
private static Guild errantGuild;
private static Guild errantNation;
private final String name;
private final GuildTag guildTag;
public final GuildCharterType charter;
private final String leadershipType; // Have to see how this is sent to the client
private final int repledgeMin;
private final int repledgeMax;
private final int repledgeKick;
private final int teleportMin;
private final int teleportMax;
private String name;
private GuildTag guildTag;
public GuildCharterType charter;
private String leadershipType; // Have to see how this is sent to the client
private int repledgeMin;
private int repledgeMax;
private int repledgeKick;
private int teleportMin;
private int teleportMax;
private final ConcurrentHashMap<Integer, Condemned> guildCondemned = new ConcurrentHashMap<>();
public boolean wooWasModified;
public HashMap<Integer, GuildAlliances> guildAlliances = new HashMap<>();
@@ -67,6 +68,13 @@ public class Guild extends AbstractWorldObject {
private String hash;
private boolean ownerIsNPC;
/**
* No-arg Constructor
*/
private Guild() {
super();
}
/**
* No Id Constructor
*/
@@ -163,6 +171,33 @@ public class Guild extends AbstractWorldObject {
this.hash = rs.getString("hash");
}
// Every NPC guild is its own nation, as live. No warehouse hash until there is one that accepts negative IDs.
public static Guild createStaticGuild(GuildSeedEntry seedEntry) {
Guild guild = new Guild();
guild.setObjectUUID(DbManager.nextStaticID(GameObjectType.Guild,
"guild:" + Integer.toUnsignedString(seedEntry.guildKeys.get(0))));
guild.setStaticWorldObject(true);
guild.name = seedEntry.name;
guild.charter = seedEntry.charter;
guild.leadershipType = seedEntry.leadershipType;
guild.guildTag = new GuildTag(seedEntry.crest[0], seedEntry.crest[1], seedEntry.crest[2],
seedEntry.crest[4], seedEntry.crest[3]);
guild.motto = seedEntry.motto;
guild.repledgeMin = seedEntry.repledgeMin;
guild.repledgeMax = seedEntry.repledgeMax;
guild.repledgeKick = seedEntry.repledgeKick;
guild.teleportMin = seedEntry.teleportMin;
guild.teleportMax = seedEntry.teleportMax;
guild.banishList = new ArrayList<>();
guild.subGuildList = new ArrayList<>();
guild.nation = guild;
guild.guildState = GuildManager.calcuateGuildState(guild);
return guild;
}
public static AbstractCharacter GetGL(Guild guild) {
if (guild == null)
return null;
+10 -12
View File
@@ -15,6 +15,8 @@ import engine.job.JobScheduler;
import engine.jobs.DeferredPowerJob;
import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
@@ -42,7 +44,6 @@ import static java.lang.Math.toIntExact;
public class Mob extends AbstractIntelligenceAgent implements Delayed {
private static int staticID = 0;
//mob specific
public final ConcurrentHashMap<Integer, Float> playerAgroMap = new ConcurrentHashMap<>(); //key = Player value = hate value
@@ -80,6 +81,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
// PERSIST.
public Mob() {
super();
this.setRotation(Quaternion.fromYaw(FastMath.PI));
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.bindLoc = Vector3fImmutable.ZERO;
@@ -99,6 +101,9 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
float statAlt;
float statLon;
// mob_rotation is not loaded: the old default (cos θ/2 = 0) faced every mob at yaw π.
this.setRotation(Quaternion.fromYaw(FastMath.PI));
try {
this.dbID = rs.getInt(1);
this.loadID = rs.getInt("mob_mobbaseID");
@@ -240,14 +245,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
writer.putVector3f(mob.getLoc());
//Rotation
float radians = (float) Math.acos(mob.getRot().y) * 2;
if (mob.building != null)
if (mob.building.getBounds() != null && mob.building.getBounds().getQuaternion() != null)
radians += (mob.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
writer.putFloat(mob.getClientYaw());
//Inventory Stuff
@@ -467,7 +465,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
Mob minionMobile;
minionMobile = new Mob();
minionMobile.currentID = (--Mob.staticID);
minionMobile.currentID = ItemManager.lastNegativeID.decrementAndGet();
minionMobile.level = level;
minionMobile.loadID = guardCaptain.loadID;
@@ -528,7 +526,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
Mob siegeMinion;
siegeMinion = new Mob();
siegeMinion.currentID = (--Mob.staticID);
siegeMinion.currentID = ItemManager.lastNegativeID.decrementAndGet();
siegeMinion.level = 1;
siegeMinion.loadID = loadID;
@@ -560,7 +558,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
Mob petMinion = new Mob();
petMinion.currentID = (--Mob.staticID);
petMinion.currentID = ItemManager.lastNegativeID.decrementAndGet();
petMinion.level = (short) (level + 20);
petMinion.loadID = loadID;
+6 -10
View File
@@ -15,7 +15,8 @@ import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
@@ -80,6 +81,7 @@ public class NPC extends AbstractCharacter {
public NPC() {
super();
this.setRotation(Quaternion.fromYaw(FastMath.PI));
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
}
@@ -118,7 +120,8 @@ public class NPC extends AbstractCharacter {
this.buyPercent = .33f;
this.sellPercent = 1;
this.setRot(new Vector3f(0, rs.getFloat("npc_rotation"), 0));
// npc_rotation holds cos θ/2
this.setRotation(Quaternion.fromYaw((float) Math.acos(rs.getFloat("npc_rotation")) * 2));
this.statLat = rs.getFloat("npc_spawnX");
this.statAlt = rs.getFloat("npc_spawnY");
@@ -320,14 +323,7 @@ public class NPC extends AbstractCharacter {
else
writer.putVector3f(npc.getLoc());
//Rotation
float radians = (float) Math.acos(npc.getRot().y) * 2;
if (npc.building != null)
if (npc.building.getBounds() != null && npc.building.getBounds().getQuaternion() != null)
radians += (npc.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
writer.putFloat(npc.getClientYaw());
//Running Speed
writer.putInt(0);
+4 -4
View File
@@ -2043,11 +2043,11 @@ public class PlayerCharacter extends AbstractCharacter {
this.stopMovement(this.getBindLoc());
} else if (bindBuilding != null) {
if (bindBuilding.getParentZone().equals(ZoneManager.findSmallestZone(this.getLoc())))
this.teleport(Ruins.getRandomRuin().getLocation());
this.teleport(ZoneManager.getRandomRuinLocation());
else
this.teleport(this.getBindLoc());
} else // no bind building found for player, teleport to ruins.
this.teleport(Ruins.getRandomRuin().getLocation());
this.teleport(ZoneManager.getRandomRuinLocation());
this.lastUpdateTime = System.currentTimeMillis();
this.lastStamUpdateTime = System.currentTimeMillis();
@@ -2144,14 +2144,14 @@ public class PlayerCharacter extends AbstractCharacter {
//handle rented room binds.
if (bindBuilding == null) {
bindLocation = mbEnums.Ruins.getRandomRuin().getLocation();
bindLocation = ZoneManager.getRandomRuinLocation();
return bindLocation;
}
bindLocation = BuildingManager.GetBindLocationForBuilding(bindBuilding);
if (bindLocation == null)
bindLocation = mbEnums.Ruins.getRandomRuin().getLocation();
bindLocation = ZoneManager.getRandomRuinLocation();
return bindLocation;
+64 -21
View File
@@ -17,6 +17,8 @@ import engine.math.Vector2f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.net.ByteBufferWriter;
import engine.parsers.objects.CZoneTemplate;
import engine.parsers.objects.ZoneTreeEntry;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
@@ -33,20 +35,24 @@ public class Zone extends AbstractWorldObject {
public final Set<NPC> zoneNPCSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
public final Set<Mob> zoneMobSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
public ZoneTemplate template;
// Server-only hotzone range for zones with icons; no client data carries zone levels
public static final int MACRO_ZONE_MIN_LEVEL = 30;
public static final int MACRO_ZONE_MAX_LEVEL = 60;
public final int playerCityUUID;
public final String zoneName;
public CZoneTemplate template;
public int playerCityUUID;
public String zoneName;
public float major_radius;
public float minor_radius;
public final float xOffset;
public final float zOffset;
public final float yOffset;
public final int templateID;
public final byte peace_zone;
public final String icon1;
public final String icon2;
public final String icon3;
public float xOffset;
public float zOffset;
public float yOffset;
public int templateID;
public byte peace_zone;
public String icon1;
public String icon2;
public String icon3;
public float absX = 0.0f;
public float absY = 0.0f;
public float absZ = 0.0f;
@@ -65,6 +71,13 @@ public class Zone extends AbstractWorldObject {
public Terrain terrain = null;
/**
* No Id Constructor
*/
public Zone() {
super();
}
/**
* ResultSet Constructor
*/
@@ -97,6 +110,36 @@ public class Zone extends AbstractWorldObject {
}
// 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(zoneID);
zone.setStaticWorldObject(true);
zone.parentZoneID = parentZoneID;
zone.templateID = treeEntry.template;
zone.zoneName = treeEntry.name;
zone.peace_zone = treeEntry.peaceZone ? (byte) 1 : (byte) 0;
zone.xOffset = treeEntry.offset.x;
zone.yOffset = treeEntry.offset.y;
zone.zOffset = treeEntry.offset.z;
zone.playerCityUUID = 0;
zone.guild_zone = false;
zone.icon1 = treeEntry.terrain;
zone.icon2 = treeEntry.biome;
zone.icon3 = treeEntry.height;
if (!zone.icon1.isEmpty()) {
zone.min_level = MACRO_ZONE_MIN_LEVEL;
zone.max_level = MACRO_ZONE_MAX_LEVEL;
}
zone.hash = DataWarehouse.hasher.encryptObjectID(zone.getObjectUUID());
return zone;
}
public static void serializeForClientMsg(Zone zone, ByteBufferWriter writer) {
if (zone.templateID == 0 && zone.playerCityUUID == 0)
@@ -168,14 +211,14 @@ public class Zone extends AbstractWorldObject {
this.minor_radius = mbEnums.CityBoundsType.ZONE.halfExtents;
this.major_radius = mbEnums.CityBoundsType.ZONE.halfExtents;
} else {
this.minor_radius = this.template.minor_radius;
this.major_radius = this.template.major_radius;
this.minor_radius = this.template.zone_minor_radius;
this.major_radius = this.template.zone_major_radius;
}
this.setParent();
this.setBounds();
if (this.template.terrain_type.equals("NONE"))
if (this.template.terrain_type == mbEnums.TerrainGenType.NONE)
this.terrain = null;
else
this.terrain = new Terrain(this);
@@ -244,15 +287,15 @@ public class Zone extends AbstractWorldObject {
return;
}
switch (this.template.sea_level_type) {
case "WORLD":
this.sea_level = world_sea_level + this.template.sea_level;
switch (this.template.zone_sea_level_type) {
case WORLD:
this.sea_level = world_sea_level + this.template.zone_sea_level;
break;
case "PARENT":
this.sea_level = this.parent.sea_level + this.template.sea_level;
case PARENT:
this.sea_level = this.parent.sea_level + this.template.zone_sea_level;
break;
case "SELF":
this.sea_level = this.global_height + this.template.sea_level;
case SELF:
this.sea_level = this.global_height + this.template.zone_sea_level;
break;
}
}
-59
View File
@@ -1,59 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.objects;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ZoneTemplate {
public int templateID;
public String zone_type;
public String zone_name;
public String peace_zone;
public float major_radius;
public float minor_radius;
public float min_blend;
public float max_blend;
public String has_water;
public String sea_level_type;
public float sea_level;
public String has_terrain;
public String terrain_type;
public float terrain_max_y;
public int terrain_image;
/**
* ResultSet Constructor
*/
public ZoneTemplate() {
}
public ZoneTemplate(ResultSet rs) throws SQLException {
this.templateID = rs.getInt("template");
this.zone_type = rs.getString("zone_type");
this.zone_name = rs.getString("zone_name");
this.major_radius = rs.getFloat("zone_major_radius");
this.minor_radius = rs.getFloat("zone_minor_radius");
this.min_blend = rs.getFloat("zone_min_blend");
this.max_blend = rs.getFloat("zone_max_blend");
this.has_water = rs.getString("zone_has_water");
this.sea_level_type = rs.getString("zone_sea_level_type");
this.sea_level = rs.getFloat("zone_sea_level");
this.has_terrain = rs.getString("zone_has_terrain_gen");
this.terrain_type = rs.getString("terrain_type");
this.terrain_max_y = rs.getFloat("terrain_max_y");
this.terrain_image = rs.getInt("terrain_image");
}
}
+344
View File
@@ -0,0 +1,344 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.cache;
import engine.gameManager.ConfigManager;
import engine.mbEnums;
import engine.parsers.objects.CZoneDrop;
import engine.parsers.objects.CZoneMobile;
import engine.parsers.objects.CZonePlacement;
import engine.parsers.objects.CZonePointSet;
import engine.parsers.objects.CZoneProp;
import engine.parsers.objects.CZoneTemplate;
import org.json.JSONArray;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
// Loads zone templates from the client cache JSON (CZONE/<id>.json), only those the map's zone tree uses.
public class ZoneParser {
private static final String zonePath = ConfigManager.DEFAULT_DATA_DIR + "CZONE/";
public static HashMap<Integer, CZoneTemplate> parseZoneTemplates(Collection<Integer> templateIds) {
HashMap<Integer, CZoneTemplate> zoneTemplates = new HashMap<>();
File zoneFile;
JSONObject zoneJson;
int failedCount = 0;
for (int templateId : templateIds) {
zoneFile = new File(zonePath + templateId + ".json");
if (!zoneFile.exists()) {
Logger.error("Zone template " + templateId + " has no CZONE file: " + zoneFile.getAbsolutePath());
failedCount++;
continue;
}
try {
zoneJson = CacheParserUtil.readJsonFile(zoneFile);
zoneTemplates.put(templateId, parseZoneEntry(templateId, zoneJson));
} catch (Exception e) {
Logger.error(zoneFile.getName() + ": " + e);
failedCount++;
}
}
Logger.info("Loaded " + zoneTemplates.size() + " of " + templateIds.size() + " zone templates");
logCounts(zoneTemplates);
// Partial zone data is never usable; null tells the caller to abort
if (failedCount > 0)
return null;
return zoneTemplates;
}
private static CZoneTemplate parseZoneEntry(int templateId, JSONObject zoneJson) {
CZoneTemplate zone = new CZoneTemplate();
JSONObject terrainJson;
JSONArray propsJson;
JSONArray mobilesJson;
JSONArray pointSetsJson;
zone.template_id = templateId;
zone.zone_type = CacheParserUtil.readEnum(mbEnums.ZoneShapeType.class, zoneJson.getString("zone_type"), templateId, "zone_type");
zone.zone_name = zoneJson.getString("zone_name");
zone.zone_peace_zone = zoneJson.getBoolean("zone_peace_zone");
zone.zone_major_radius = zoneJson.getFloat("zone_major_radius");
zone.zone_minor_radius = zoneJson.getFloat("zone_minor_radius");
zone.zone_min_blend = zoneJson.getFloat("zone_min_blend");
zone.zone_max_blend = zoneJson.getFloat("zone_max_blend");
zone.zone_sea_level = zoneJson.getFloat("zone_sea_level");
zone.zone_sea_level_type = CacheParserUtil.readEnum(mbEnums.SeaLevelType.class, zoneJson.getString("zone_sea_level_type"),
templateId, "zone_sea_level_type");
zone.zone_has_water = zoneJson.getBoolean("zone_has_water");
zone.zone_has_terrain_gen = zoneJson.getBoolean("zone_has_terrain_gen");
zone.terrain_type = mbEnums.TerrainGenType.NONE;
if (zone.zone_has_terrain_gen) {
terrainJson = zoneJson.getJSONObject("zone_terrain_gen");
zone.terrain_type = CacheParserUtil.readEnum(mbEnums.TerrainGenType.class, terrainJson.getString("terrain_type"),
templateId, "terrain_type");
if (zone.terrain_type == mbEnums.TerrainGenType.TARGA) {
zone.terrain_max_y = terrainJson.getFloat("terrain_max_y");
zone.terrain_image = terrainJson.getInt("terrain_image");
}
}
propsJson = zoneJson.getJSONArray("zone_prop_info");
for (int i = 0; i < propsJson.length(); i++)
zone.zone_prop_info.add(parseProp(propsJson.getJSONObject(i), templateId));
mobilesJson = zoneJson.getJSONArray("zone_mobile_info");
for (int i = 0; i < mobilesJson.length(); i++)
zone.zone_mobile_info.add(parseMobile(mobilesJson.getJSONObject(i), templateId));
pointSetsJson = zoneJson.getJSONArray("zone_point_sets");
for (int i = 0; i < pointSetsJson.length(); i++)
zone.zone_point_sets.add(parsePointSet(pointSetsJson.getJSONObject(i), templateId));
return zone;
}
private static CZonePlacement parsePlacement(JSONObject placementJson, int templateId) {
CZonePlacement placement = new CZonePlacement();
JSONObject sparseJson;
placement.basic_zone_spawn_location = CacheParserUtil.readVector3(placementJson.getJSONArray("basic_zone_spawn_location"));
placement.basic_zone_name_override = placementJson.getString("basic_zone_name_override");
placement.basic_zone_time_to_respawn = placementJson.getFloat("basic_zone_time_to_respawn");
placement.basic_zone_spawn_radius = placementJson.getFloat("basic_zone_spawn_radius");
placement.basic_zone_y_rot = placementJson.getFloat("basic_zone_y_rot");
placement.basic_zone_template_id = (int) placementJson.getLong("basic_zone_template_id");
placement.basic_zone_level_number = placementJson.getInt("basic_zone_level_number");
placement.basic_zone_room_number = placementJson.getInt("basic_zone_room_number");
sparseJson = placementJson.getJSONObject("basic_zone_sparse_data");
for (String key : sparseJson.keySet()) {
if (!key.equals("IMMUNE_TO_STEAL"))
throw new IllegalArgumentException(templateId + ": basic_zone_sparse_data has unknown key '" + key + "'");
placement.basic_zone_immune_to_steal = sparseJson.getBoolean(key);
}
return placement;
}
private static CZoneProp parseProp(JSONObject propJson, int templateId) {
CZoneProp prop = new CZoneProp();
JSONObject dataJson;
JSONArray childrenJson;
JSONArray mobilesJson;
prop.prop_type = CacheParserUtil.readEnum(mbEnums.ZonePropType.class, propJson.getString("prop_type"), templateId, "prop_type");
dataJson = propJson.getJSONObject("prop_data");
prop.prop_basic_zone = parsePlacement(dataJson.getJSONObject("prop_basic_zone"), templateId);
childrenJson = dataJson.getJSONArray("prop_content_props");
for (int i = 0; i < childrenJson.length(); i++)
prop.prop_content_props.add(parseProp(childrenJson.getJSONObject(i), templateId));
mobilesJson = dataJson.getJSONArray("prop_content_mobiles");
for (int i = 0; i < mobilesJson.length(); i++)
prop.prop_content_mobiles.add(parseMobile(mobilesJson.getJSONObject(i), templateId));
prop.prop_registry = dataJson.getInt("prop_registry");
if (prop.prop_registry != 0)
prop.prop_registry_name = dataJson.getString("prop_registry_name");
prop.prop_has_teleporter = dataJson.getBoolean("prop_has_teleporter");
if (prop.prop_has_teleporter)
prop.prop_teleporter = (int) dataJson.getLong("prop_teleporter");
if (prop.prop_type == mbEnums.ZonePropType.CITY) {
prop.city_prop_gm_guild = (int) dataJson.getLong("city_prop_gm_guild");
prop.city_prop_gm_nation = (int) dataJson.getLong("city_prop_gm_nation");
prop.city_prop_asset_rank = dataJson.getInt("city_prop_asset_rank");
prop.city_prop_asset_funds = dataJson.getInt("city_prop_asset_funds");
}
return prop;
}
private static CZoneMobile parseMobile(JSONObject mobileJson, int templateId) {
CZoneMobile mobile = new CZoneMobile();
JSONObject dataJson;
JSONArray equipmentJson;
JSONArray equipJson;
JSONArray bootiesJson;
JSONObject bootyJson;
JSONArray itemsJson;
String rootFsm;
mobile.mobile_type = CacheParserUtil.readEnum(mbEnums.ZoneMobileType.class, mobileJson.getString("mobile_type"), templateId, "mobile_type");
dataJson = mobileJson.getJSONObject("mobile_data");
mobile.mobile_base_zone = parsePlacement(dataJson.getJSONObject("mobile_base_zone"), templateId);
mobile.mobile_number_in_group = dataJson.getInt("mobile_number_in_group");
mobile.mobile_level = dataJson.getInt("mobile_level");
mobile.mobile_dialog_override = (int) dataJson.getLong("mobile_dialog_override");
mobile.mobile_rune_stone_ids.addAll(CacheParserUtil.readIntList(dataJson.getJSONArray("mobile_rune_stone_ids")));
mobile.mobile_spawn_hps.addAll(CacheParserUtil.readIntList(dataJson.getJSONArray("mobile_spawn_hps")));
// Equipment entries are [item id, chance]
equipmentJson = dataJson.getJSONArray("mobile_equipment");
for (int i = 0; i < equipmentJson.length(); i++) {
equipJson = equipmentJson.getJSONArray(i);
mobile.mobile_equipment.add(new CZoneDrop((int) equipJson.getLong(0), equipJson.getFloat(1)));
}
// Booty items are [chance, item id] pairs, the reverse of equipment
bootiesJson = dataJson.getJSONArray("mobile_booties");
for (int i = 0; i < bootiesJson.length(); i++) {
bootyJson = bootiesJson.getJSONObject(i);
if (!bootyJson.getString("type").equals("ITEM"))
throw new IllegalArgumentException(templateId + ": mobile_booties has unknown type '" + bootyJson.getString("type") + "'");
itemsJson = bootyJson.getJSONArray("items");
for (int j = 0; j + 1 < itemsJson.length(); j += 2)
mobile.mobile_booties.add(new CZoneDrop((int) itemsJson.getLong(j + 1), itemsJson.getFloat(j)));
}
rootFsm = dataJson.getString("mobile_root_fs_mid");
if (!rootFsm.isEmpty())
mobile.mobile_root_fs_mid = CacheParserUtil.readEnum(mbEnums.MobBehaviourType.class, rootFsm, templateId, "mobile_root_fs_mid");
mobile.mobile_has_home_goal = dataJson.getBoolean("mobile_has_home_goal");
if (mobile.mobile_has_home_goal) {
mobile.mobile_home_goal = (int) dataJson.getLong("mobile_home_goal");
mobile.mobile_spawn_home_goal = dataJson.getBoolean("mobile_spawn_home_goal");
}
mobile.mobile_has_target_goal = dataJson.getBoolean("mobile_has_target_goal");
if (mobile.mobile_has_target_goal) {
mobile.mobile_target_goal = (int) dataJson.getLong("mobile_target_goal");
mobile.mobile_spawn_target_goal = dataJson.getBoolean("mobile_spawn_target_goal");
}
mobile.mobile_has_group_tactics = dataJson.getBoolean("mobile_has_group_tactics");
if (mobile.mobile_has_group_tactics)
mobile.mobile_group_tactics = (int) dataJson.getLong("mobile_group_tactics");
mobile.mobile_enemy_monster_types.addAll(CacheParserUtil.readEnumSet(mbEnums.MonsterType.class,
dataJson.getJSONArray("mobile_enemy_monster_types"), templateId, "mobile_enemy_monster_types"));
mobile.mobile_not_enemy_monster_types.addAll(CacheParserUtil.readEnumSet(mbEnums.MonsterType.class,
dataJson.getJSONArray("mobile_not_enemy_monster_types"), templateId, "mobile_not_enemy_monster_types"));
mobile.mobile_friendly_charters.addAll(CacheParserUtil.readIntList(dataJson.getJSONArray("mobile_friendly_charters")));
mobile.mobile_parley_name = dataJson.getString("mobile_parley_name");
if (mobile.mobile_type == mbEnums.ZoneMobileType.TRAINER) {
mobile.trainer_std_profit_margin = dataJson.getFloat("trainer_std_profit_margin");
mobile.trainer_profit_margin = dataJson.getFloat("trainer_profit_margin");
}
return mobile;
}
private static CZonePointSet parsePointSet(JSONObject pointSetJson, int templateId) {
CZonePointSet pointSet = new CZonePointSet();
JSONArray pointsJson;
JSONObject pointJson;
CZonePointSet.Point point;
pointSet.zone_goal_type = CacheParserUtil.readEnum(mbEnums.PointSetType.class, pointSetJson.getString("zone_goal_type"),
templateId, "zone_goal_type");
pointSet.zone_goal_value = (int) pointSetJson.getLong("zone_goal_value");
pointSet.zone_goal_name = pointSetJson.getString("zone_goal_name");
pointsJson = pointSetJson.getJSONArray("zone_goal_points");
for (int i = 0; i < pointsJson.length(); i++) {
pointJson = pointsJson.getJSONObject(i);
point = new CZonePointSet.Point();
point.zone_goal_point_location = CacheParserUtil.readVector3(pointJson.getJSONArray("zone_goal_point_location"));
point.zone_goal_point_vectors = CacheParserUtil.readIntArray(pointJson.getJSONArray("zone_goal_point_vectors"));
pointSet.zone_goal_points.add(point);
}
return pointSet;
}
// Counts to check against World-Boot-Plan findings §1
private static void logCounts(HashMap<Integer, CZoneTemplate> zoneTemplates) {
int topProps = 0;
int childProps = 0;
int cityProps = 0;
int zoneMobiles = 0;
int zoneTrainers = 0;
int propMobiles = 0;
int propTrainers = 0;
int pointSets = 0;
for (CZoneTemplate zone : zoneTemplates.values()) {
topProps += zone.zone_prop_info.size();
pointSets += zone.zone_point_sets.size();
for (CZoneMobile mobile : zone.zone_mobile_info) {
if (mobile.mobile_type == mbEnums.ZoneMobileType.TRAINER)
zoneTrainers++;
else
zoneMobiles++;
}
for (CZoneProp prop : zone.zone_prop_info) {
childProps += prop.prop_content_props.size();
if (prop.prop_type == mbEnums.ZonePropType.CITY)
cityProps++;
for (CZoneMobile mobile : prop.prop_content_mobiles) {
if (mobile.mobile_type == mbEnums.ZoneMobileType.TRAINER)
propTrainers++;
else
propMobiles++;
}
}
}
Logger.info("Zone props: " + topProps + " top level (" + cityProps + " CITY), " + childProps + " children");
Logger.info("Zone mobiles: " + zoneMobiles + " MOBILE, " + zoneTrainers + " TRAINER; prop mobiles: "
+ propMobiles + " MOBILE, " + propTrainers + " TRAINER; point sets: " + pointSets);
}
}
+110
View File
@@ -0,0 +1,110 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.map;
import engine.gameManager.ConfigManager;
import engine.mbEnums;
import engine.parsers.cache.CacheParserUtil;
import engine.parsers.objects.GuildSeed;
import engine.parsers.objects.GuildSeedEntry;
import org.json.JSONArray;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
import java.io.File;
// Loads the NPC guild seed for a map (maps/<map>.guild), in file order. Guild keys are unsigned in the file.
public class GuildSeedParser {
private static final String mapPath = ConfigManager.DEFAULT_DATA_DIR + "maps/";
public static GuildSeed parseGuildSeed(int mapID) {
GuildSeed guildSeed = new GuildSeed();
File seedFile = new File(mapPath + mapID + ".guild");
JSONObject seedJson;
JSONArray guildsJson;
JSONArray noGuildJson;
JSONArray ruinJson;
if (!seedFile.exists()) {
Logger.error("Guild seed not found: " + seedFile.getAbsolutePath());
return null;
}
try {
seedJson = CacheParserUtil.readJsonFile(seedFile);
if (seedJson.getInt("map") != mapID) {
Logger.error(seedFile.getName() + ": map " + seedJson.getInt("map") + " does not match MB_WORLD_MAPID " + mapID);
return null;
}
guildsJson = seedJson.getJSONArray("guilds");
for (int i = 0; i < guildsJson.length(); i++)
guildSeed.guilds.add(parseGuildEntry(guildsJson.getJSONObject(i)));
noGuildJson = seedJson.getJSONArray("noGuildKeys");
for (int i = 0; i < noGuildJson.length(); i++)
guildSeed.noGuildKeys.add((int) noGuildJson.getLong(i));
ruinJson = seedJson.getJSONArray("ruinKeys");
for (int i = 0; i < ruinJson.length(); i++)
guildSeed.ruinKeys.add((int) ruinJson.getLong(i));
} catch (Exception e) {
Logger.error(seedFile.getName() + ": " + e);
return null;
}
Logger.info("Loaded " + guildSeed.guilds.size() + " guilds from " + seedFile.getName());
return guildSeed;
}
private static GuildSeedEntry parseGuildEntry(JSONObject guildJson) {
GuildSeedEntry guild = new GuildSeedEntry();
JSONArray keysJson = guildJson.getJSONArray("guildKeys");
JSONArray crestJson = guildJson.getJSONArray("crest");
for (int i = 0; i < keysJson.length(); i++)
guild.guildKeys.add((int) keysJson.getLong(i));
guild.nationKey = (int) guildJson.getLong("nationKey");
guild.name = guildJson.getString("name");
guild.charter = mbEnums.GuildCharterType.valueOf(guildJson.getString("charter"));
guild.leadershipType = guildJson.getString("leadershipType");
guild.motto = guildJson.getString("motto");
guild.repledgeMin = guildJson.getInt("repledgeMin");
guild.repledgeMax = guildJson.getInt("repledgeMax");
guild.repledgeKick = guildJson.getInt("repledgeKick");
guild.teleportMin = guildJson.getInt("teleportMin");
guild.teleportMax = guildJson.getInt("teleportMax");
guild.noobIsle = guildJson.optBoolean("noobIsle", false);
if (guild.guildKeys.isEmpty())
throw new IllegalStateException("guild " + guild.name + " has no guild keys");
if (crestJson.length() != 5)
throw new IllegalStateException("guild " + guild.name + " crest has " + crestJson.length() + " values");
guild.crest = new int[5];
for (int i = 0; i < 5; i++)
guild.crest[i] = crestJson.getInt(i);
return guild;
}
}
@@ -0,0 +1,86 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.map;
import engine.gameManager.ConfigManager;
import engine.parsers.cache.CacheParserUtil;
import engine.parsers.objects.ZoneTreeEntry;
import org.json.JSONArray;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
import java.io.File;
import java.util.ArrayList;
// Loads the zone tree for a map (maps/<map>.tree), in file order: parents always precede their children.
public class ZoneTreeParser {
private static final String mapPath = ConfigManager.DEFAULT_DATA_DIR + "maps/";
public static ArrayList<ZoneTreeEntry> parseZoneTree(int mapID) {
ArrayList<ZoneTreeEntry> zoneTree = new ArrayList<>();
File treeFile = new File(mapPath + mapID + ".tree");
JSONObject treeJson;
JSONArray zonesJson;
if (!treeFile.exists()) {
Logger.error("Zone tree not found: " + treeFile.getAbsolutePath());
return null;
}
try {
treeJson = CacheParserUtil.readJsonFile(treeFile);
if (treeJson.getInt("map") != mapID) {
Logger.error(treeFile.getName() + ": map " + treeJson.getInt("map") + " does not match MB_WORLD_MAPID " + mapID);
return null;
}
zonesJson = treeJson.getJSONArray("zones");
for (int i = 0; i < zonesJson.length(); i++)
zoneTree.add(parseZoneEntry(zonesJson.getJSONObject(i)));
} catch (Exception e) {
Logger.error(treeFile.getName() + ": " + e);
return null;
}
Logger.info("Loaded " + zoneTree.size() + " zones from " + treeFile.getName());
return zoneTree;
}
private static ZoneTreeEntry parseZoneEntry(JSONObject zoneJson) {
ZoneTreeEntry zone = new ZoneTreeEntry();
zone.uid = zoneJson.getInt("uid");
zone.parent = zoneJson.getInt("parent");
zone.template = zoneJson.getInt("template");
zone.name = zoneJson.getString("name");
zone.offset = CacheParserUtil.readVector3(zoneJson.getJSONArray("offset"));
zone.peaceZone = zoneJson.getBoolean("peaceZone");
zone.terrain = zoneJson.getString("terrain");
zone.biome = zoneJson.getString("biome");
zone.height = zoneJson.getString("height");
// The server places zones by offset alone; a rotated zone would put everything in it out of place
if (zoneJson.getFloat("yaw") != 0.0f)
throw new IllegalStateException("zone " + zone.uid + " has a non-zero yaw");
if (zone.offset == null)
throw new IllegalStateException("zone " + zone.uid + " has no offset");
return zone;
}
}
+24
View File
@@ -0,0 +1,24 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
// One item with a chance, from a CZONE mobile's mobile_equipment or mobile_booties.
// Equipment chances are 0–1 fractions; booty chances are percentages (100.0, 50.0).
public class CZoneDrop {
public final int item_id;
public final float chance;
public CZoneDrop(int item_id, float chance) {
this.item_id = item_id;
this.chance = chance;
}
}
@@ -0,0 +1,59 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.mbEnums;
import java.util.ArrayList;
import java.util.EnumSet;
// A MOBILE or TRAINER entry from a CZONE zone (zone_mobile_info) or from a
// top-level prop (prop_content_mobiles). Field names match the JSON keys.
// Fields that are default in every shipped file are not parsed (Task 1.1 list).
public class CZoneMobile {
public mbEnums.ZoneMobileType mobile_type;
public CZonePlacement mobile_base_zone;
public int mobile_number_in_group;
public int mobile_level;
public int mobile_dialog_override;
public final ArrayList<Integer> mobile_rune_stone_ids = new ArrayList<>();
public final ArrayList<CZoneDrop> mobile_equipment = new ArrayList<>();
public final ArrayList<CZoneDrop> mobile_booties = new ArrayList<>();
public final ArrayList<Integer> mobile_spawn_hps = new ArrayList<>();
// null when the entry has no root FSM
public mbEnums.MobBehaviourType mobile_root_fs_mid;
// Goal values are zone_point_sets zone_goal_value hashes
public boolean mobile_has_home_goal;
public int mobile_home_goal;
public boolean mobile_spawn_home_goal;
public boolean mobile_has_target_goal;
public int mobile_target_goal;
public boolean mobile_spawn_target_goal;
public boolean mobile_has_group_tactics;
public int mobile_group_tactics;
public final EnumSet<mbEnums.MonsterType> mobile_enemy_monster_types = EnumSet.noneOf(mbEnums.MonsterType.class);
public final EnumSet<mbEnums.MonsterType> mobile_not_enemy_monster_types = EnumSet.noneOf(mbEnums.MonsterType.class);
public final ArrayList<Integer> mobile_friendly_charters = new ArrayList<>();
public String mobile_parley_name;
// TRAINER only
public float trainer_std_profit_margin;
public float trainer_profit_margin;
}
@@ -0,0 +1,32 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.math.Vector3fImmutable;
// The basic_zone_* block shared by every CZONE prop and mobile
// (client BasicZoneTemplateData). Field names match the JSON keys.
// spawn_location is relative to the parent; y_rot is not the client yaw (W8: yaw = π − y_rot).
public class CZonePlacement {
public Vector3fImmutable basic_zone_spawn_location;
public String basic_zone_name_override;
public float basic_zone_time_to_respawn;
public float basic_zone_spawn_radius;
public float basic_zone_y_rot;
public int basic_zone_template_id;
public int basic_zone_level_number;
public int basic_zone_room_number;
// IMMUNE_TO_STEAL is the only sparse key in the shipped cache
public boolean basic_zone_immune_to_steal;
}
@@ -0,0 +1,36 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import java.util.ArrayList;
// A zone_point_sets entry: a named path that mobile home/target goals point at
// through zone_goal_value. Field names match the JSON keys.
public class CZonePointSet {
public mbEnums.PointSetType zone_goal_type;
public int zone_goal_value;
public String zone_goal_name;
public final ArrayList<Point> zone_goal_points = new ArrayList<>();
public static class Point {
public Vector3fImmutable zone_goal_point_location;
// Six ints; the first is the point's index, the rest are unknown
public int[] zone_goal_point_vectors;
}
}
+39
View File
@@ -0,0 +1,39 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.mbEnums;
import java.util.ArrayList;
// A PROP or CITY entry from a CZONE zone (zone_prop_info) or a child prop
// (prop_content_props). Field names match the JSON keys.
// Children never have children or mobiles in the shipped cache.
public class CZoneProp {
public mbEnums.ZonePropType prop_type;
public CZonePlacement prop_basic_zone;
public final ArrayList<CZoneProp> prop_content_props = new ArrayList<>();
public final ArrayList<CZoneMobile> prop_content_mobiles = new ArrayList<>();
public int prop_registry;
public String prop_registry_name = "";
public boolean prop_has_teleporter;
public int prop_teleporter;
// CITY only. Guild and nation are keys into the map's guild seed (W7).
public int city_prop_gm_guild;
public int city_prop_gm_nation;
public int city_prop_asset_rank;
public int city_prop_asset_funds;
}
@@ -0,0 +1,51 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.mbEnums;
import java.util.ArrayList;
import java.util.HashMap;
// Zone template as defined by the client's cache data (MBDATA/data/cache/CZONE).
// Field names match the JSON keys. Only server-needed fields are parsed (Task 1.1 list);
// textures, water, weather, sound and fields default in every file are skipped.
// A template is reused by many zones; placement lives in the zone tree.
//
// Read only once loaded.
public class CZoneTemplate {
public static HashMap<Integer, CZoneTemplate> _zones = new HashMap<>();
public int template_id;
public mbEnums.ZoneShapeType zone_type;
public String zone_name;
public boolean zone_peace_zone;
public float zone_major_radius;
public float zone_minor_radius;
public float zone_min_blend;
public float zone_max_blend;
public float zone_sea_level;
public mbEnums.SeaLevelType zone_sea_level_type;
public boolean zone_has_water;
// zone_terrain_gen; max_y and image exist only on TARGA terrain
public boolean zone_has_terrain_gen;
public mbEnums.TerrainGenType terrain_type;
public float terrain_max_y;
public int terrain_image;
public final ArrayList<CZoneProp> zone_prop_info = new ArrayList<>();
public final ArrayList<CZoneMobile> zone_mobile_info = new ArrayList<>();
public final ArrayList<CZonePointSet> zone_point_sets = new ArrayList<>();
}
+24
View File
@@ -0,0 +1,24 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import java.util.ArrayList;
import java.util.HashSet;
// A map's server-owned NPC guild seed (maps/<map>.guild). Guilds are in static ID order.
// noGuildKeys are placeholder CITY-prop guild keys. ruinKeys mark the Ruins' trees: no guild either, and
// their zones are where unbound characters respawn when MB_USE_RUINS is on.
public class GuildSeed {
public final ArrayList<GuildSeedEntry> guilds = new ArrayList<>();
public final HashSet<Integer> noGuildKeys = new HashSet<>();
public final HashSet<Integer> ruinKeys = new HashSet<>();
}
@@ -0,0 +1,34 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.mbEnums;
import java.util.ArrayList;
// One NPC guild of a <map>.guild seed file. guildKeys and nationKey are CZONE city_prop_gm_guild / _nation values.
// Crest is in obj_guild column order: backgroundColor01, backgroundColor02, symbolColor, backgroundDesign, symbol.
public class GuildSeedEntry {
public final ArrayList<Integer> guildKeys = new ArrayList<>();
public int nationKey;
public String name;
public int[] crest;
public mbEnums.GuildCharterType charter;
public String leadershipType;
public String motto;
public int repledgeMin;
public int repledgeMax;
public int repledgeKick;
public int teleportMin;
public int teleportMax;
public boolean noobIsle;
}
@@ -0,0 +1,28 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.parsers.objects;
import engine.math.Vector3fImmutable;
// One zone of a <map>.tree file, exported by mbScryer from a recording's NEWWORLD message.
// Field names match the JSON keys; terrain, biome and height are the zone's three icons.
public class ZoneTreeEntry {
public int uid;
public int parent;
public int template;
public String name;
public Vector3fImmutable offset;
public boolean peaceZone;
public String terrain;
public String biome;
public String height;
}
@@ -135,12 +135,12 @@ public class PowerActionParser {
break;
case SetItemFlag:
powerAction.item_flag = header.enumToken(mbEnums.ItemFlags.class, 2);
powerAction.item_flag_value = header.optionalBoolToken(3);
powerAction.item_flag_value = header.optionalIntToken(3) != 0;
break;
case Track:
powerAction.effect_ids.add(header.token(2));
powerAction.track_player = header.boolToken(3);
powerAction.track_corpse = header.boolToken(4);
powerAction.track_player = header.intToken(3) != 0;
powerAction.track_corpse = header.intToken(4) != 0;
powerAction.track_filter = header.token(5);
powerAction.max_track = header.intToken(7);
break;
@@ -175,7 +175,7 @@ public class PowerActionParser {
powerAction.transfer_amount_curve = WpakParserUtil.curveToken(header, 5);
powerAction.transfer_efficiency = header.floatToken(6);
powerAction.transfer_efficiency_curve = WpakParserUtil.curveToken(header, 7);
powerAction.transfer_target_to_caster = header.boolToken(8);
powerAction.transfer_target_to_caster = header.intToken(8) != 0;
powerAction.isaggressive = header.optionalIntToken(9) != 0;
}
@@ -186,13 +186,13 @@ public class PowerActionParser {
switch (line.token(0)) {
case "ISAGGRESSIVE=":
powerAction.isaggressive = line.optionalBoolToken(1);
powerAction.isaggressive = line.optionalExactTrueToken(1);
break;
case "BLADETRAILS=":
powerAction.bladetrails = line.optionalBoolToken(1);
powerAction.bladetrails = line.optionalExactTrueToken(1);
break;
case "ISRESISTABLE=":
powerAction.isresistable = line.optionalBoolToken(1);
powerAction.isresistable = line.optionalExactTrueToken(1);
break;
case "APPLYEFFECTSELF=":
powerAction.applyeffectself = line.token(1);
@@ -225,10 +225,10 @@ public class PowerActionParser {
powerAction.femalebodyparts.addAll(line.intTokensFrom(1));
break;
case "SHOULDSHOWWEAPONS=":
powerAction.shouldshowweapons = line.optionalBoolToken(1);
powerAction.shouldshowweapons = line.optionalExactTrueToken(1);
break;
case "SHOULDSHOWARMOR=":
powerAction.shouldshowarmor = line.optionalBoolToken(1);
powerAction.shouldshowarmor = line.optionalExactTrueToken(1);
break;
case "ATTACKANIMS=":
powerAction.attackanims.addAll(line.intTokensFrom(1));
+30 -9
View File
@@ -20,8 +20,8 @@ import java.util.regex.Pattern;
public class WpakLine {
// The client reads numbers like C atoi/atof: the leading number counts, the rest is dropped
// (Powers.cfg has "2.9.0" for a float and "10.0" for an int)
// The client reads numbers with strtol/strtod (Core.dll String::ToInt/ToFloat): the leading
// number counts, the rest is dropped, so "2.9.0" is 2.9 and "10.0" as an int is 10
private static final Pattern intPrefix = Pattern.compile("[-+]?\\d+");
private static final Pattern floatPrefix = Pattern.compile("[-+]?(\\d+\\.?\\d*|\\.\\d+)");
@@ -107,23 +107,44 @@ public class WpakLine {
return intToken(index);
}
// TRUE/FALSE, or a number where non-zero is true
// Core.dll String::ToBool with the false default every wpak loader passes: a leading '0' is
// false, '1'..'9' is true, then TRUE/FALSE in any case; anything else reads as false
public boolean boolToken(int index) {
String value = token(index);
char first = value.isEmpty() ? 0 : value.charAt(0);
if (first == '0')
return false;
if (first >= '1' && first <= '9')
return true;
if (value.equalsIgnoreCase("TRUE"))
return true;
if (value.equalsIgnoreCase("FALSE"))
if (!value.equalsIgnoreCase("FALSE"))
Logger.warn(location() + ": token " + index + " '" + value + "' read as false, as the client does");
return false;
}
// Some PowerActions keys compare the token to "TRUE" exactly (String_EqualsCStr 0x546450), so "1" is false
public boolean optionalExactTrueToken(int index) {
String value;
if (!has(index))
return false;
try {
return Integer.parseInt(value) != 0;
} catch (NumberFormatException e) {
throw error("token " + index + " '" + value + "' is not a boolean");
}
value = token(index);
if (!value.equals("TRUE") && !value.equals("FALSE"))
Logger.warn(location() + ": token " + index + " '" + value + "' read as false, as the client does");
return value.equals("TRUE");
}
public boolean optionalBoolToken(int index) {
+127 -18
View File
@@ -36,6 +36,8 @@ import engine.parsers.cache.DeedParser;
import engine.parsers.cache.ItemParser;
import engine.parsers.cache.ParityCheck;
import engine.parsers.cache.RuneParser;
import engine.parsers.cache.ZoneParser;
import engine.parsers.objects.CZoneTemplate;
import engine.parsers.objects.DeedTemplate;
import engine.parsers.objects.ItemTemplate;
import engine.parsers.objects.RuneTemplate;
@@ -136,14 +138,20 @@ public class WorldServer {
if (retVal != 0)
Logger.error(
".exec() returned value: '" + retVal);
exit(retVal);
exitServer(retVal);
} catch (Exception e) {
Logger.error(e.getMessage());
exit(1);
exitServer(1);
}
}
// The tinylog writing thread drops queued lines on System.exit; flush them so boot aborts are logged
private static void exitServer(int status) {
Configurator.shutdownWritingThread(true);
exit(status);
}
public static String getUptimeString() {
String outString = null;
@@ -278,6 +286,13 @@ public class WorldServer {
private boolean init() {
mbEnums.StaticBuildingMode staticBuildingMode;
int staticZoneRows;
int npcCityRows = 0;
int movedStaticIDs;
ArrayList<StaticBuildingLoader.StaticBuildingEntry> staticBuildingEntries = null;
ArrayList<StaticCityLoader.StaticCityEntry> staticCityEntries = null;
Logger.info("MAGICBANE SERVER GREETING:");
Logger.info(ConfigManager.MB_WORLD_GREETING.getValue());
@@ -296,15 +311,12 @@ public class WorldServer {
Logger.info("Initializing Errant Guild");
Guild.getErrantGuild();
Logger.info("Loading zone template data");
DbManager.ZoneQueries.LOAD_ALL_ZONE_TEMPLATES();
Logger.info("Loading Compound Curves");
CompoundCurveTemplate._curves = CompoundCurveParser.parseCompoundCurvesFile();
if (CompoundCurveTemplate._curves == null || CompoundCurveTemplate._curves.isEmpty()) {
Logger.error("ABORT! Compound curves failed to load!");
exit(1);
exitServer(1);
}
CurveRamp.verifyRampForms();
@@ -314,7 +326,7 @@ public class WorldServer {
if (ModTypeTemplate._modTypes == null || ModTypeTemplate._modTypes.isEmpty()) {
Logger.error("ABORT! Mod types failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Power Actions");
@@ -322,7 +334,7 @@ public class WorldServer {
if (PowerActionTemplate._powerActions == null || PowerActionTemplate._powerActions.isEmpty()) {
Logger.error("ABORT! Power actions failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Effects");
@@ -330,7 +342,7 @@ public class WorldServer {
if (EffectTemplate._effects == null || EffectTemplate._effects.isEmpty()) {
Logger.error("ABORT! Effects failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Powers");
@@ -338,7 +350,7 @@ public class WorldServer {
if (PowerTemplate._powers == null || PowerTemplate._powers.isEmpty()) {
Logger.error("ABORT! Powers failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Skills");
@@ -346,7 +358,7 @@ public class WorldServer {
if (SkillTemplate._skills == null || SkillTemplate._skills.isEmpty()) {
Logger.error("ABORT! Skills failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading SkillsBase");
@@ -357,7 +369,7 @@ public class WorldServer {
if (StartingKitTemplate._kits == null || StartingKitTemplate._kits.isEmpty()) {
Logger.error("ABORT! Starting kits failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Character Creation Runes");
@@ -365,7 +377,7 @@ public class WorldServer {
if (CharCreateRuneList._runeList == null) {
Logger.error("ABORT! Character creation runes failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Power Action Resource Costs");
@@ -373,7 +385,7 @@ public class WorldServer {
if (PowActionCostTemplate._costs == null || PowActionCostTemplate._costs.isEmpty()) {
Logger.error("ABORT! Power action resource costs failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Initializing PowersManager.");
@@ -408,7 +420,7 @@ public class WorldServer {
if (DeedTemplate._deeds == null || DeedTemplate._deeds.isEmpty()) {
Logger.error("ABORT! Deed templates failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Rune Templates");
@@ -416,7 +428,7 @@ public class WorldServer {
if (RuneTemplate._runes == null || RuneTemplate._runes.isEmpty()) {
Logger.error("ABORT! Rune templates failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Loading Cache Item Templates");
@@ -424,12 +436,29 @@ public class WorldServer {
if (ItemTemplate._items == null || ItemTemplate._items.isEmpty()) {
Logger.error("ABORT! Cache item templates failed to load!");
exit(1);
exitServer(1);
}
Logger.info("Indexing Cache Templates");
CObjectIndex.build();
Logger.info("Loading Zone Tree");
if (!ZoneManager.loadZoneTree(worldMapID)) {
Logger.error("ABORT! Zone tree failed to load!");
exitServer(1);
}
Logger.info("Loading Cache Zone Templates");
CZoneTemplate._zones = ZoneParser.parseZoneTemplates(ZoneManager.getZoneTreeTemplateIDs());
if (CZoneTemplate._zones == null || CZoneTemplate._zones.isEmpty()) {
Logger.error("ABORT! Cache zone templates failed to load!");
exitServer(1);
}
ZoneManager.loadZoneTemplates();
Logger.info("Loading Rune Power Requirements");
PowerReq.loadRunePowers();
@@ -526,17 +555,97 @@ 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);
}
if (StaticManifest.isRenumberPending(worldMapID)) {
Logger.error("ABORT! maps/" + worldMapID + ".renumber.sql exists: apply it with World-Boot-Plan sql/static_renumber.sql, then delete it.");
exitServer(1);
}
Logger.info("Loading NPC guilds");
if (!GuildManager.loadStaticGuilds(worldMapID)) {
Logger.error("ABORT! Guild seed failed to load!");
exitServer(1);
}
Logger.info("Loading all Zones");
DbManager.ZoneQueries.GET_ALL_ZONES();
if (!ZoneManager.loadZones(staticBuildingMode == mbEnums.StaticBuildingMode.JSON)) {
Logger.error("ABORT! Zones failed to load!");
exitServer(1);
}
GuildManager.logMissingGuildKeys();
ZoneManager.loadRuinZones();
if (staticBuildingMode == mbEnums.StaticBuildingMode.JSON) {
Logger.info("Loading static Buildings from the zone JSON");
staticBuildingEntries = StaticBuildingLoader.buildStaticBuildings(true);
}
Logger.info("Loading all Buildings");
BuildingManager._npcBuildingOwners = DbManager.BuildingQueries.GET_NPC_BUILDING_OWNERS();
DbManager.BuildingQueries.GET_ALL_BUILDINGS();
// NPC cities need the static trees. Until the 2.5 migration deletes the DB NPC cities, they load from the
// DB and the JSON cities only take their IDs for the parity check.
if (staticBuildingMode == mbEnums.StaticBuildingMode.JSON) {
npcCityRows = DbManager.StaticWorldQueries.COUNT_NPC_CITY_ROWS();
if (npcCityRows < 0) {
Logger.error("ABORT! NPC city rows could not be counted.");
exitServer(1);
}
Logger.info("Static cities: " + npcCityRows + " NPC city rows in obj_city; " + (npcCityRows == 0 ? "loading from the zone JSON" : "parity only"));
staticCityEntries = StaticCityLoader.buildStaticCities(staticBuildingEntries, npcCityRows == 0);
}
Logger.info("Loading all Cities");
DbManager.CityQueries.GET_ALL_CITIES();
Logger.info("Loading all NPCs");
DbManager.NPCQueries.GET_ALL_NPCS();
// NPCs are cached now; Building.runAfterLoad falls back to the NPC cache.
BuildingManager._npcBuildingOwners.clear();
Logger.info("Loading all Mobiles");
DbManager.MobQueries.GET_ALL_MOBS();
if (staticBuildingMode == mbEnums.StaticBuildingMode.PARITY) {
Logger.info("Comparing static buildings with the zone JSON");
StaticBuildingParity.run(worldMapID);
}
if (npcCityRows > 0) {
Logger.info("Comparing NPC cities with the zone JSON");
StaticCityParity.run(worldMapID, staticCityEntries);
}
movedStaticIDs = StaticManifest.writeManifest(worldMapID);
StaticManifest.logOrphanReferences();
// DB references still hold the old IDs; play would put them on the wrong objects.
if (movedStaticIDs > 0) {
Logger.error("ABORT! " + movedStaticIDs + " static IDs moved. Apply maps/" + worldMapID
+ ".renumber.sql with World-Boot-Plan sql/static_renumber.sql, delete it, then boot again.");
exitServer(1);
}
Logger.info("time to load World Objects: " + (System.currentTimeMillis() - start) + " ms");
// Configure realms for serialization
+10
View File
@@ -245,6 +245,16 @@ public class Hasher {
return encode(numbers, alphabet_, salt_, minHashLength_);
}
// encode() fails on negative numbers, and static world objects have negative IDs. A static ID hashes as
// the pair (0, -id), which no single positive ID can produce.
public String encryptObjectID(long objectID) {
if (objectID < 0)
return encrypt(0, -objectID);
return encrypt(objectID);
}
public long[] decrypt(String hash) {
return decode(hash);
}