Compare commits

...
Author SHA1 Message Date
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
MagicBotandClaude Opus 5.5 15437246ce Remove the DB power requirement loader
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:22:15 -04:00
MagicBotandClaude Opus 5.5 3f6c75b363 Import ErrorPopupMsg in RefineMsgHandler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:09:01 -04:00
MagicBotandClaude Opus 5.5 9e50d0a5ad Build power requirements from rune item_power_grant
PowerReq lists now come from the rune JSON on world and login: level from
power_value, skill and power prereqs from power_granted_skills/powers.
The DB table is no longer read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:42:09 -04:00
MagicBotandClaude Opus 5.5 24e91cc0a9 Block skill refines that break a trained power's skill requirement
The old check looked up power requirements by power id instead of rune id,
so it never ran. Refining a skill below a trained power's requirement let
calculatePowers delete the power and its trains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:41:23 -04:00
MagicBotandClaude Opus 5.5 17d82066d6 Remove the DB power action list loader
Deletes ActionsBase.getActionsBase (static_power_action) and the
ResultSet constructor; the 4.9 switch (2a19a851) builds the lists from
Powers.cfg and was tested. No value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:26:04 -04:00
MagicBotandClaude Opus 5.5 2a19a85108 Build power action lists from Powers.cfg ACTION= lines
ActionsBase(ActionEntry, powerID, id, powerAction) with the duration ramp
from CurveRamp; PowersManager.loadActionLists fills PowersBase.actions in
Powers.cfg order (ids 1..2278 + 10000), then the OPENGATE actions
(5001-5008, unchanged). StackType gains the 10 cfg stack types it lacked
and loses the DB-only ResistanceDebuff spelling.

Parity (JSON-Refactor-Plan/scripts/p49.py, dev export, every column):
2287 DB rows vs 2278 cfg lines; 1453/1455 powers have the same action
list; all curves have a ramp form, every action id exists.

Value changes (cfg wins):
- TRA-003..010 lose their DB OPENGATE row; the synthetic OPENGATE action
  was already added, so each gate power applied OPENGATE twice
- HFG-001 loses the DB-only third action HFG-001B (Str +25, attack
  delay -15, melee damage +15)
- SCT-004 applies SCT-004A (Speed SL0125Up, Buff/Effect) instead of
  RNG-033A (Speed SL0100Up, also Chant)
- durations: ALF-001A 180->60, COM-001A 60->30, FGM-002A/003A
  1800->900, RNG-033A 60->30
- stack priority: DEF/OFF/PRE/NOSTANCE A and B Always->GreaterThanOrEqualTo,
  BRD-016A and PRS-031A GreaterThan->GreaterThanOrEqualTo
- stack types: the 16 resist debuffs split from ResistanceDeBuff/
  ResistanceDebuff into Cold/Fire/Lightning/Holy/Unholy/Magic/Mental
  DeBuff (FUR-029A IgnoreStack->ElementalDeBuff), so debuffs of different
  elements now stack; HNT-023A..C StackableAttr*Buff->PersonalAttr*Buff
- IgnoreStack effect keys: action line ids follow cfg order (runtime only)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:20:34 -04:00
MagicBotandClaude Opus 5.5 f44f1ca138 Remove the DB power, cast prereq and power fail condition loaders
getAllPowersBase, getFailConditions, PowerPrereq.getAllPowerPrereqs and
the PowersBase/PowerPrereq ResultSet constructors. Powers come from
Powers.cfg since ac72d2c8 (tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:09:31 -04:00
MagicBotandClaude Opus 5.5 ac72d2c8d2 Build powers, cast prereqs and power fail conditions from Powers.cfg
PowersBase(PowerTemplate, id) and PowerPrereq template constructors;
PowersManager.loadPowers fills both maps on the world and login servers
(ids 1..1465 in file order, identical to the DB IDs). The 3.10 target
type exceptions and applyClientTargetTypes are gone; the dead
BLEED-DOT-10.5-RANGE token override is removed (the power is
BLEED-DOT-10.5-RANGED and its DB token is already its hash).

Parity (JSON-Refactor-Plan/scripts/p48.py, every column): 1465/1465 ids,
tokens = SBStringHash(id), flags 1465/1465, prereqs 720/720 rows, fail
conditions 103/105 powers. Absent MAXNUM*TARGETS stay 7 (patch notes).

Value changes (cfg wins, user 2026-10-26):
- target types HLR-025 PC->CORPSE, HNT-023 PC->SELF, RNG-033 GROUP->SELF
- HNT-023 SPELL->SKILL, target select CLICK->NONE
- cast time RNG-033 3->0.2, ROG-002 10->12, WIZ-067 6->7
- WIZ-067 release 1->0, init ramp 0->-0.13; SCT-004 cost ramp 0.5->0
- recycle ALF-001 300->180, FGM-002/003 600->900
- WLR-018 category HEAL->none
- BRD-016, PRS-031 lose the power CastSpell fail condition
- 4 name typos and 167 descriptions from the cfg; 2 effect prereq
  messages (SAB-001, SAP-004) untruncated (unread)
- canCastWhileFlying true by default (client ctor; no live reader)
- category BOON-CLASS/BOON-RACE -> BOONCLASS/BOONRACE: GetSourceType now
  resolves them instead of logging an error (no effect blocks them)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:57:46 -04:00
MagicBotandClaude Opus 5.5 3cf966d0eb Keep Powers.cfg file order and the client's power key defaults
PowerParser fills a LinkedHashMap so boot-assigned power IDs follow the
file order (the same order as static_power_powerbase IDs 1..1465).
PowerTemplate defaults follow ArcPower_ctor 0x56CD40: MAXLEVEL 9999,
CANCASTWHILEFLYING, SHOULDCHECKPATH and CANTARGETADMIN TRUE. No reader
yet; every power sets MaxLevel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:55:38 -04:00
MagicBotandClaude Opus 5.5 025a451770 Remove the DB power action loader
Power actions come from PowerActions.cfg (3fc5e725, tested). Deletes
AbstractPowerAction.getAllPowerActions and the ResultSet constructor on
every power action class. static_power_poweraction is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:41:17 -04:00
MagicBotandClaude Opus 5.5 29e1e51b04 Check the teleport block against the power's category
City spire effects give players BlockedPowerType TELEPORT. TeleportPowerAction
checked TELEPORT for every teleport, so Infiltrate (SAB-001, category
SPIREPROOFTELEPORT) passed the cast check but never moved. Use the power's
category as finishUsePower does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:26:32 -04:00
MagicBotandClaude Opus 5.5 093b68258b Fall back to the default swing anim for weapons with none
310 cache weapons have empty attack anim lists, which threw
IndexOutOfBoundsException in getSwingAnimation when a guard swung one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:24:10 -04:00
MagicBotandClaude Opus 5.5 3fc5e7253a Build power actions from PowerActions.cfg
PowersManager.loadPowerActions builds every AbstractPowerAction from
PowerActionTemplate (template ctor on all subclasses), in id order with IDs
1..3046; OPENGATE keeps 5000. The ResultSet loader stays until removal.
The parser now reads the SimpleDamage (min, max), Teleport (bool), Track
(two bools) and TransferStat (bool at token 8) header fields.

Parity (JSON-Refactor-Plan/scripts/p47.py, every column): ids 3046/3046 plus
the DB-only AELFBORN row, which nothing references (the cfg has
BOON-AELFBORN). Effect ids, deferred powers, RemoveEffect source/REMOVEALL,
track filter/max, transfer types/ramps/ramp-add, mob level identical. The old
flags match the new header fields: Track 1024/2048 = tokens 3/4,
TransferStat 16384 = token 8, Teleport 32768 = SAB-001A's TRUE.

Value changes (PowerActions.cfg wins):
- DamageOverTime/TransferStatOT: the cfg value is the tick count; tick length
  = action duration / ticks. The DB stored cfg+1 as tick seconds, which only
  worked for 30s DoTs (5 x 6s, unchanged). HOLY-DOT-17 (60s) goes 10 -> 5
  ticks, BLEED-DOT (4s) 0 -> 4, TransferStatOT 30s 6 -> 5 ticks.
- TransferStat amounts are read with ToInt as the client does: 16 actions
  drop by 0.5 (BEL-001A 31.5 -> 31, CON-001A 13.5 -> 13, ...).
- PRE-334..337 become ApplyEffects with one effect; ApplyEffectsPowerAction
  now allows a missing second effect on baked-in items.
- Charm level cap ramp from the LEVELCAP= curve (same values as the DB).
- Charm actions with effect FEAR/CONFUSION (12, DB type Confusion) stay
  ConfusionPowerAction no-ops; CHARM ones use CharmPowerAction (decided).
- CreateMob: the cfg client mob id (49..74, 3500, 3501) maps to the mobbase
  through a map in CreateMobPowerAction, from the old DB values.
- The 50 Charm/Block/SpireDisable rows with no DB effect id get the cfg id
  (unread by those classes); isAggressive differs on 330 rows (unread).
- SimpleDamage keeps min/max instead of the always-0 simpleDamage (unread).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 05:37:36 -04:00
MagicBotandClaude Opus 5.5 b5c3d8c3fd Read the Charm/Fear level cap ramp as a float, fix the Track flag tests
levelCapRamp is a float column (e.g. 0.65) but was read with getInt, so the
Charm level cap never grew with trains. Charm caps now add ramp * trains.
FearPowerAction gets the same fix; nothing builds it today.

TrackPowerAction tested (flags & 1024) == 1, which is always false. No
caller reads trackPlayer/trackCorpse, so that part changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 05:33:29 -04:00
MagicBotandClaude Opus 5.5 dee936d8a5 Remove the DB effect loaders
Effects, modifiers, source types, effect fail conditions and item
enchant values all come from Effects.cfg (4.6b). Removes
dbEffectsBaseHandler, dbPowerHandler, the EffectsBase and effect
modifier ResultSet constructors, EffectsBase.getFailConditions and
LOAD_ENCHANT_VALUES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 05:17:24 -04:00
MagicBotandClaude Opus 5.5 e2ec8eb8a6 Build effect modifiers from Effects.cfg
Each EffectsBase builds its modifiers from its EffectTemplate MODS
lines, with ramps from the CompoundCurve helper. The Value mods now
fill Item.enchantValues, replacing LOAD_ENCHANT_VALUES. The ItemName
skip list of DB row IDs is gone; it only hid Centurion's and
Imperturbable from ./makeitem.

Value changes (cfg wins): weapon power X effects lose the DB-only OCV
mod; PRE-210A..237A resists and SUF-153A..171A resists (doubled);
Speed on SPRINT, SNT-010A, BHN-001A, HNT/RNG-037A, BRD-016A, PRS-031A,
and RNG-049A loses Speed; ARM-058A Necromancy +1; TMP-040A, WLR-018A;
PARRY/CRU-032A gain their Parry line. RESTORE's damage type is NONE
(cfg typo, as the client reads it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:40:43 -04:00
MagicBotandClaude Opus 5.5 27c59749fa Build EffectsBase from Effects.cfg
Effects, their fail conditions and source types come from
EffectTemplate. IDs are assigned at boot in file order. The power
actions that the server also keys as effects (item enchantment parents
and the action tokens sent in effect messages) are built from the
ApplyEffect/ApplyEffects/Invis actions with no Effects.cfg block: 776
keys, the 712 old registry rows plus 64 such as BRD-014A, which live
also sent.

Value changes (cfg wins): BRD-016A/PRS-031A lose the Chant source,
RUC-002A..004A lose Buff/Effect, and PRS-031A/RNG-037A no longer break
on Attack. Effect modifiers still load from the DB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:37:58 -04:00
MagicBotandClaude Opus 5.5 f8d764b5f5 Keep action line IDs apart from effect IDs
ActionsBase IDs (IgnoreStack effect keys) and EffectsBase IDs are both
written as Integer.toString keys into the same effects map, and their
ranges overlapped. Action lines now start at 10001. Also drops the
unused EffectsBase copy constructor and its NewID counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:35:33 -04:00
MagicBotandClaude Opus 5.5 073417567f Remove the dead power 334 chant branches
Both branches of the getUUID() != 334 checks were identical, and they
tied the code to a DB row ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:35:10 -04:00
MagicBotandClaude Opus 5.5 c450af73af Remove the SafeMode and INVIS-D overrides from SBStringHash
The client hash (0x5547E0) has no overrides. Hashing the power id
SafeMode must give the real power token for 4.8. Source types are now
keyed by the effect's own token, which keeps the SafeModeA/B override
in EffectsBase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 04:34:35 -04:00
MagicBotandClaude Opus 5.5 aef3212b06 Remove the static_effect_costmaps loader
Deletes dbEffectsResourceCostHandler, EffectsResourceCosts and
DbManager.EffectsResourceCostsQueries. GameObjectType.EffectsResourceCosts
stays because the enum ordinals are sent to the client and stored in the DB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:58:39 -04:00
MagicBotandClaude Opus 5.5 4f323ae12d Build effect cost maps from PowActionCostInfo.cfg
PowersManager._effect_costMaps now comes from PowActionCostTemplate
instead of static_effect_costmaps. Parity (scripts/p45.py, dev DB):
524/524 ids, 522 identical. Value changes, cfg wins (D2, user
decision 2026-10-26):
- PRE-092 Yglandor: Sulfur 1 -> 6
- PRE-097 Jagged: adds Sulfur 1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:58:15 -04:00
MagicBotandClaude Opus 5.5 337cc3d360 Remove the dead power animation override and valid item flag code
PowersManager.AnimationOverrides was loaded from
static_power_animation_override and never read. validItemFlags on
AbstractPowerAction was always 0 and its getter had no callers; its
loader (static_power_effect_allowed_item) was already commented out.
No value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:42:38 -04:00
MagicBotandClaude Opus 5.5 637a798191 Remove the SkillsBase ResultSet constructor
SkillsBase comes from Skills.cfg since ff52d1d7 (tested). Drop the
ResultSet ctor; static_skill_skillsbase is shrunk to ID, name
(sql/4.3-skillsbase-shrink.sql, run on mbdb_108 and patched into
magicbane.sql).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:31:16 -04:00
MagicBotandClaude Opus 5.5 ff52d1d7e5 Build SkillsBase from Skills.cfg
SkillsBase name, description, token (SBStringHash of the name) and
attribute weights now come from SkillTemplate._skills. The skill ids
stay in static_skill_skillsbase (ID, name), which dyn_character_skill
and SkillReq use; the loader joins each row's name to the cfg skill and
logs an error for a name with no cfg entry. Both servers load all 63 at
boot after Skills.cfg; GET_BASE/GET_BASE_BY_NAME/GET_BASE_BY_TOKEN read
the boot caches only. The other 31 cfg skills stay in SkillTemplate.

Parity (scripts/p43.py, dev DB): 63/63 names, 63/63 tokens, 63/63
weights. Only value change: description was empty for all 63 in the DB
and is now the cfg text (no server reader).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:14:19 -04:00
MagicBotandClaude Opus 5.5 9af6dd30ea Remove the static_rune_validkit loader
Kits come from StartingKitTable.cfg since c1e1ed91 (tested); drop
dbKitHandler, DbManager.KitQueries, the Kit ResultSet ctor and
RaceClassIDMap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:04:05 -04:00
MagicBotandClaude Opus 5.5 c1e1ed9148 Build kits from StartingKitTable.cfg
Kit is built from StartingKitTemplate._kits and looked up by race name,
sex and class name like the client's kit map (0x620250), replacing the
hard-coded racebaseclass IDs in GetKitIDByRaceClass. Equip slots come
from each item's item_eq_slots_or, filled from the last cfg item so the
weapon keeps RHELD. dbKitHandler and the ResultSet ctor stay until the
removal commit.

Parity (scripts/p42.py, dev static_rune_validkit vs cfg): 68/68 headers,
451/451 item sets, 449/451 kits with identical slots. Value changes:
- Irekei female Mage kit 3 (182460/185090/29970) was kitNumber 4 in the
  DB, so the client's kit 3 was refused at creation; now selectable.
- Dwarf male Fighter kits 4 and 5 list two boots; the DB put Black Cuffed
  Boots (185120) in CHEST. They are a FEET item, so they are now skipped
  with a warning and Black Riding Boots (185090) stay in FEET.
- Old Half-Giant male (2019) now finds the Half-Giant male kits by name
  instead of a missing map entry.
Noob gear item set unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:44:13 -04:00
MagicBotandClaude Opus 5.5 5824f34a79 Take creation talents from CharCreateRuneList ATTRRUNES
Replaces the hard-coded NON_CREATION_TALENTS exclusions. ATTRRUNES (84)
equals the 92 flagged creation talents minus exactly those 8 runes, so
the talent lists are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:22:48 -04:00
MagicBotandClaude Opus 5.5 4cc45f1304 Add a CompoundCurve to (ramp, add) helper with a boot check
FlatLine -> (0, add), SlopedLine p -> (p, add), SlopedInitValLine p ->
(p, no add), checked against every static_power_effectmod and
static_power_action row (6228 rows; the 11 ramp value diffs are the
known 4.0 effectmod diffs). DefaultCurveUp/Down have no ramp form and
no mod or action uses them. The world boot checks five known DB rows
and logs curves without a ramp form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:22:48 -04:00
MagicBotandClaude Opus 5.5 f409e4ee4c Load creation and power cfgs on the login server
CompoundCurves (needed by the Powers.cfg parser), Powers, Skills,
StartingKitTable and CharCreateRuneList now load at login boot from the
shared mb.data/wpak. No consumer reads them yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:22:48 -04:00
MagicBotandClaude Opus 5.5 2da7c190bc Load the wpak cfgs before PowersManager on the world server
The wpak parse now runs ahead of initPowersManager, so the Powers.cfg
target type step runs at the end of InitializePowers instead of as a
separate call after the wpak load. No value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:22:48 -04:00
MagicBotandClaude Opus 5.5 8bb6cd8268 Send the Wall Guard rune to wall archers
Wall archers are now sent 252622 Wall Guard in place of 252623 Standing
Guard (serialize and getClientRuneIDs). The rune roots its holder (ROOT2,
ROOTFSMID=Power), which matches wall archers never moving (canRoam false).

No server value change: 252622 and 252623 add the same +100 to all five
attributes with the same max, the only fields the server reads from the
sent runes. Wall archers don't cast, so the 252623 power rank steps don't
apply to them. The rune's effects and its MOB-SNARE grant are not applied
by the server, as with the other guard runes.

Client side: 252623 is the only rune that sets the client's is-guard flag
(0x67AB1B), so the CloseToGuard hint no longer fires near wall archers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:31:03 -04:00
MagicBotandClaude Opus 5.5 b4a15ebf60 Take guard spell rank from the Standing Guard rune steps
The client ranks a power by adding every POWERSADJ (item_power_action)
step at or below the character's level (0x4B34B0). The Standing Guard
rune's Wizard steps (10@10, 10@20, 10@30, 5@40..70) now set the rank of
every captain spell, capped at the power's MaxLevel as player casts are.

Value change by guard rank 1-7:
  before 10/15/20/25/30/35/40
  after  10/20/30/35/40/40/40
All 28 captain spells have MaxLevel 40; the uncapped sum would be 50.
Using one curve for the non-Wizard faction spells and the cap are user
decisions (Task 3.9 follow-up 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:44:08 -04:00
MagicBotandClaude Opus 5.5 717166e655 Send the Standing Guard rune to guards and contract mobs
The client sets its is-guard flag only for rune 252623 (0x67AB1B), and
that flag drives the CloseToGuard passive help hint. The server sent
252621 Guard, so the hint never fired. Contract mobs and player guards
now get 252623 in place of 252621.

No server value change: both runes add +100 to all five attributes
with the same max, the only fields the server reads from the sent runes.
The runes differ only in their effects (252621 MOVE-B-15%, OCV-B-20%,
RES-ELEMENTS-B-50; 252623 GUARD), which the server never applied.
The dead 100570/100962/100965 branches keep 252621.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:28:32 -04:00
MagicBotandClaude Opus 5.5 ac5d1548b7 Remove the static_rune_powers loader
Nothing reads the table since the captain powers moved to MinionType.
Drops LOAD_RUNE_POWERS and the ResultSet RunePowerEntry constructor.
The table drop is MBDATA sql/3.9-rune-powers-drop.sql, run after deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:35:34 -04:00
MagicBotandClaude Opus 5.5 dff53d5c76 Move guard captain powers from static_rune_powers to MinionType
The 14 Magister Captain contracts' powers have no client rune or deed
(Task 3.9), so they live on the MinionType mage entries instead of the
DB fallback in loadAllRunePowers. canCast, mobCast and aiInfoCmd read
them through MinionType.getCaptainPowers.

No value change: same 56 power ids, rank 40, and all 56 DB tokens equal
the powerbase token of their power id (checked on mbdb_108).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 12:34:02 -04:00
MagicBotandClaude Opus 5.5 b0460ed2d0 Derive mob attributes from every rune the client is sent
Client ArcCharacter_RecalcMaxAttributes (0x470440) and RecalcAttributes
(0x470610) treat the stats in the mob message as spent points and add
rune_attr_adj from all of the character's runes, clamped to
[1, max(1, sum of rune_max_attr_adj)]. The server summed only the race
rune and sent the result, so the client counted it twice.

Mob.getClientRuneIDs lists the runes serializeForClientMsgOtherPlayer
sends (race, plus Pet 2522 for pets, Warrior 2518 + Guard 252621 for
contract and guard mobs); getRuneAttribute applies the client rule.
calculateModifiedStats and the mob branch of CharacterSkill use it,
and the message now sends 0 (no spent points), as NPCs already do.
MobBaseStats (race rune only) has no users left and is removed.

Value changes (server side):
- Contract mobs and player guards: +100 to all five attributes
  (Guard rune 252621; its max adj is also +100, so no new clamp).
- Pets: + Pet rune 2522 (Str 76, Dex 76, Con 120, Int 40, Spi 40;
  max Str 500, Dex 500, Con 2000, Int 40, Spi 40).
- The new max clamp changes no loaded mobbase on its own (checked all
  2375 against their race runes).
These feed mob skill values (CharacterSkill) and statXxxCurrent.
Guard ATR/defense/damage/health still come from the NPCManager guard
formulas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:42:48 -04:00
MagicBotandClaude Opus 5.5 5c58586f89 Send mob stats under the right attribute hashes
The mob message paired Dex with the Spirit hash (0xACB82E33), Int with
the Dexterity hash (0xE07B3336) and Spi with the Intelligence hash
(0xFF665EC3); the inline comments were wrong. Verified with
Hasher.SBStringHash. Mob and NPC now use MBServerStatics.STAT_*_ID.
Value change: the client now gets a mob's Dex, Int and Spi in the right
slots. NPCs send 0 for all five, so no change there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:40:06 -04:00
MagicBotandClaude Opus 5.5 3a1450286f Take mob scale from the race rune instead of static_npc_mobbase.scale
The client builds a mob's scale from its race rune SCALEFACTOR
(ArcCharacter_BuildCharFromRune 0x458160) and overwrites the three floats
the server sends, so the DB column (always 1) never had a visible effect.
MobBase now holds rune_scale_factor (1,1,1 when absent) and the mob message
sends it. No client-visible change. The column drop is
sql/3.7-mobbase-scale-drop.sql, to run after this build is deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:39:36 -04:00
MagicBotandClaude Opus 5.5 236bddc93b Key MobBase on its ID and stop reading loadID
Every static_npc_mobbase row has ID = loadID since 3.6 dropped the
server-made rows, so the race rune and rune effects are looked up by ID and
the loaders no longer filter on loadID. No value change (dev: 2375 of 2375
rows have ID = loadID). Mob.loadID and NPC.loadID are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:37:16 -04:00
MagicBotandClaude Opus 5.5 7027000ba1 Remove the dead static_npc_mobbase_race speeds loader
LOAD_ALL_MOBBASE_SPEEDS and MobBase.updateSpeeds have had no caller since
3.3 moved mob speeds to rune_speed. No value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:36:30 -04:00
MagicBotandClaude Opus 5.5 f07d7f3fe9 Remove the dead static_npc_mob_resists loader
AbstractCharacter.getResists fell back to Resists.getResists(0), but the
table has no ID 0, so the fallback always returned null. It now returns
the field directly. Removes dbResistHandler, the cache, the ResultSet
constructor and the empty setMobResists stub. No value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:11:30 -04:00
MagicBotandClaude Opus 5.5 6561d585b9 Remove the dead static_npc_mobbase_stats loader
MobBase builds its stats from the race rune's rune_attr_adj since 3.2, so
LOAD_STATS and the MobBaseStats ResultSet constructor have no callers.
No value change. The table is dropped by sql/3.6-mob-stats-resists-drop.sql.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:11:00 -04:00
MagicBotandClaude Opus 5.5 b2730794a2 Show effect details in /printeffects
Each line now has the effects map key (effect UUID or stack type such as
PetBuff), the effect id and name, rank, time left or static, and the
power that applied it. Help and usage strings no longer describe
/printstats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:27:27 -04:00
MagicBotandClaude Opus 5.5 c8653626a2 Use a Java 8 set for the DB target type powers
The dev build compiles as Java 8, which has no Set.of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:03:29 -04:00
MagicBotandClaude Opus 5.5 a6f2495e57 Compare monster type restrictions as MonsterType
PowersBase.monsterTypeRestrictions is now an EnumSet<MonsterType>
(DB values Siege/Centaur/Dwarf/Vampire read with CacheParserUtil.readEnum).
MobBase gets monsterType from its race rune's rune_sub_type (all 2380
RACE runes convert); players use RaceType.getMonsterType(). No string
compares and no rune lookup per cast. Same results as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:59:37 -04:00
MagicBotandClaude Opus 5.5 7b58211386 Enforce MONSTERTYPERESTRICTS on player targets
Players use their race rune's rune_sub_type, like mobs. Only GOR-003
Crimson Pact (single-target ghoul transformation) restricts player
types: it no longer lands on Centaur 2004/2005, Dwarf 2006 or Vampire
2028/2029, matching its description ("Cannot be cast on a vampire,
centaur, or dwarf"). No player race rune has sub-type Siege, so the
Siege-restricted PC/GROUP powers are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:57:04 -04:00
MagicBotandClaude Opus 5.5 86d78a55a9 Enforce MONSTERTYPERESTRICTS on mob targets
static_power_powerbase monsterTypeRestrict1-3 were loaded into
PowersBase.monsterTypeRestrictions but never read. getAllTargets now
drops a Mob whose race rune rune_sub_type is restricted (the rune
sub-type is the client's monster type, g_MonsterTypeEnum at rune +0xC).

86 powers restrict Siege (DB and Powers.cfg agree). Value changes:
- Imbue Golem, Dark Lord's Boon, Strengthen Beast no longer land on the
  Siege-type summons Siege Golem 11999 (ANM-004) and Ice Fiend 11998
  (SUM-005); they still buff the other pets.
- The 83 Siege-restricted harmful powers (bleeds, DOTs, procs, debuffs,
  Challenge, The Lesser Kiss, ...) no longer affect Trebuchet 13171,
  Mangonel 13757, Ballista 13758 or the two Siege summons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:56:22 -04:00
MagicBotandClaude Opus 5.5 3a9fc2607a Accept single-target pet powers on pets
validateTarget tested MASK_MOB before MASK_PET, and summoned pets carry
both, so every single-target PET power (pet heals and buffs such as
Repair Golem, Dark Lord's Favor, Strengthen Beast, Vashteera's Favor)
was rejected on any pet. Pets are now tested first and accept PET or
MOBILE powers, matching the client, where Mobile covers every ArcMobile
and Pet is left to the server (0x49E440). Non-pet mobs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:52:57 -04:00
MagicBotandClaude Opus 5.5 f7324831b8 Remove the NECROPET power target type
No power uses it after DRU-066 moved to the Powers.cfg Pet type, and its
area query (WorldGrid.getObjectsInRangePartialNecroPets) was an empty
stub. The DB rows are aligned with sql/necro-pet-targeting.sql.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:51:41 -04:00
MagicBotandClaude Opus 5.5 fe5c69cc09 Give necro pets MASK_PET
DRU-066 Braialla's Judgement targets Pet and requires NECROPETSTATS, so
necro pets must be found by the Pet AoE query; the client leaves Pet
targeting to the server (0x49E440 has no Pet case). Reverts the necro
pet exclusion from 40799f96.

Other MASK_PET readers: MobAI pet aggro scan is still off
(PET_AGGRO_ENABLED = false); InterestManager and validateTarget test
MASK_MOB first, so they don't change. DRU-066 is the only AoE PET power.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:50:34 -04:00
MagicBotandClaude Opus 5.5 72dcf07aab Take power target types from Powers.cfg
PowersBase gets setTargetType (the old inline parse, now resetting the
flags first) and the world server re-applies every power's target type
from PowerTemplate after the wpak load. The login server only uses power
tokens and keeps the DB value.

Parity vs static_power_powerbase (1465 powers): 1460 identical, area,
radius type, exclude and target effect prereqs identical for all.
Value changes:
- NEC-018 Sepulchre's Bounty: PET -> MOBILE. Its AoE now collects mobs
  and pets; the NECROPETSTATS target prereq keeps only necro pets.
- DRU-066 Braialla's Judgement: NECROPET -> PET.
Kept on the DB (user, behaviour change, for the Phase 4 power task):
HLR-025 Succor PC (cfg CORPSE), HNT-023 Phaedra's Fury PC (cfg SELF),
RNG-033 Fleet of Foot GROUP (cfg SELF).

Client: ArcPower_IsValidTargetForType 0x49E440 accepts any live
ArcMobile for Mobile and has no case for Pet (server decides).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:49:39 -04:00
MagicBotandClaude Opus 5.5 ad3e616cb7 Remove the static_npc_mobbase_effects loader
Mob effects come from the race rune (2.10b builder) and necro pets get
NECROPETSTATS in code, so GET_RUNEBASE_EFFECTS, the MobBaseEffects
ResultSet constructor, its unused mobBaseID and the equals/hashCode used
for the merge are gone. The table is now unread; drop it with
sql/3.4-mobbase-effects-drop.sql.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:16:19 -04:00
MagicBotandClaude Opus 5.5 fb7e641d9c Grant NECROPETSTATS to necro pets in code
No client data grants NECROPETSTATS (no rune, summon power or item; no
reference in sb.exe), but NEC-018 Sepulchre's Bounty and DRU-066
Braialla's Judgement require it on the target, so the server adds it to
Grobones 12021 and Wight 12022 in applyMobbaseEffects.

Rank is the highest rank of the pet's own ladder (PET_NECRO /
PET_NECRO_HARDHIT) at or below its level, which is exactly what the 100
server-made static_npc_mobbase_effects rows held (same 50 rank/reqLvl
pairs). Tying it to that ladder is a guess at the official behaviour.
No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:15:32 -04:00
MagicBotandClaude Opus 5.5 2593e397a9 Note why the necro pets keep their DB effects rows
static_npc_mobbase_effects is shrunk to the 100 NECROPETSTATS rows
(sql/3.4-mobbase-effects-shrink.sql); the Wild Boar 13236 PEACE row is
dropped as unused (no spawns, contracts or summon power).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:07:45 -04:00
MagicBotandClaude Opus 5.5 4bf3cf9a27 Load mob effects from the race rune's item_user_power_action
MobBase now takes its effects from MobBaseEffects.getEffectsForRune (the
2.10b builder) and adds the DB rows the rune doesn't have on top.

Parity (scripts/p34.py, 2375 mobbases): 1028 identical, 1344 empty on
both sides, 3 with server-only DB rows that stay in the DB:
- 12021 Grobones, 12022 Wight: 50 NECROPETSTATS rows each
- 13236 Wild Boar: PEACE rank 40, reqLvl 1
No value changes. Effect order does not matter: applyMobbaseEffects keeps
the highest eligible rank per effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:54:19 -04:00
MagicBotandClaude Opus 5.5 063ebb9717 Load mob speeds from the race rune's rune_speed
MobBase walk/walkCombat/run/runCombat now come from rune_speed
WALK/COMBATWALK/RUN/COMBATRUN instead of static_npc_mobbase_race.

Parity (JSON-Refactor-Plan/scripts/p33.py, compared as Java floats):
2375/2375 loaded mobbases identical, every one has a race row on dev.
No value changes. The 253 unused race rows (no mobbase) and the
fly/swim columns had no reader.

LOAD_ALL_MOBBASE_SPEEDS and static_npc_mobbase_race are now unread;
they go in Task 3.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:44:32 -04:00
MagicBotandClaude Opus 5.5 12fa84d379 Remove the dead mobbase base-skill path
static_npc_mobbase_stats.baseSkills never matches a static_skill_skillsbase
token on dev (2007496699 on 2208 rows is no known skill hash and is absent
from the client), so applyMobbaseSkill never added a skill and only cost a
skillsbase query per mob spawn. No value changes. Nothing reads the
baseSkills/skillAmount columns now; the table goes in 3.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:33:34 -04:00
MagicBotandClaude Opus 5.5 06a421de0f Load MobBaseStats from the race rune's rune_attr_adj
Parity (scripts/p32.py, static_npc_mobbase_stats by loadID vs rune_attr_adj):
2301 of 2375 mobbases identical. 74 have no stats row and went from all-zero
stats to the rune values: CSR 2020/2021, summoner pets 12024-12035 and
13266-13301, clay constructs 12200-12202/12375/12382, Guard 13170 (not spawned
or contracted), necro undead 13339-13348, and 13961, 14167, 14168, 14260-14262,
14285, 14292. On dev 47 obj_mob spawns use these (13343, 14167, 14168, 14285,
14292). Effect: higher rune skill values for these mobs via CharacterSkill.
LOAD_STATS stays until the table is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:32:59 -04:00
MagicBotandClaude Opus 5.5 ccc9d9f0ed Drop the server-made mobbase rows and the dead mobbase columns
The 406 ID <> loadID mobbases (15000-17266) were junk: no RUNE file, nothing
on dev references them. MobBase now always reads its core fields from the
race rune and logs an error if the rune is missing; the loader skips any
ID <> loadID rows still present. GetClassType only matched those IDs, so it
always returned 2518 and is replaced by the constant.

DB: MBDATA/JSON-Refactor-Plan/sql/3.6-mobbase-cleanup.sql deletes the rows,
the 10 orphan static_npc_mobbase_effects rows (17061/17122/17154) and drops
13 columns no longer read: lootTableID, name, level, health, atr, defense,
minDmg, maxDmg, noaggro, raceType, fsm, notEnemy, enemy.

Value changes: none for the 2375 loaded mobbases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:03:27 -04:00
MagicBotandClaude Opus 5.5 c811ba9f4e Log the loaded mobbase count, split into rune and server-made rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:31:49 -04:00
MagicBotandClaude Opus 5.5 c74cb127dc Load MobBase core fields from the race rune
Mobbases whose ID is their own race rune (2375 of 2781) now take name,
level, health, atr, defense, min/max damage, fsm and enemy/notEnemy from
the rune; the 406 server-made mobbases keep their DB row. Server-only
columns (bootySet, flags, seeInvis, spawnTime, goldMod, scale,
attackRange) stay DB.

Parity (MBDATA scripts/p31.py vs magicbane.sql), JSON for all (D2,
user decision 2026-10-23):
- fsm 84: DB None where the rune has no ROOTFSMID; no change
- enemy/notEnemy: no change
- level 95: rune 0 (player races, guards, pets, golems, necro pets)
- level 8: 13314 53->42, 13581 53->40, 13758 78->40, 14034 54->40,
  14172 54->40, 14174 55->40, 14177 54->40, 13171 10->1
- health/atr/def/dmg 61: DB defaults 100/10/20 -> 0 (tombstones
  2030-2040, pets 12024-12035 and 13266-13301, CSR 2020/2021 atr/def/dmg)
- defense 27: DB 100 -> rune 140-780 (clay, risen/doom, skeletal lines,
  12011, 13961, 14167, 14168, 14285, 14292); damage/atr there and on
  14037/14087/14296/14303 already came from the rune at runtime
- name 33: DB names -> obj_name (e.g. Sharduk -> Hell Hound)

No ParityCheck comparator: p31.py covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:12:12 -04:00
MagicBotandClaude Opus 5.5 0c7b6f6c46 Compute XP thresholds from the client formula instead of the LevelToExp table
Client ArcCharacter_ExpForLevelUp (0x4715D0): XP to reach level L is
(L-1)^3 * max(1, 1 + (L-40) * 0.0512f) * class * race rune_exp_req_to_level,
rounded half up. getLevel steps levels with it up to LEVELCAP. Both methods
now take the PlayerCharacter so the factors come from its runes; the CSR
promotions 2901+ have no rune and use the base class factor.

Same values: every player race is 10 and every class rune 15, and the formula
equals the removed table for all 76 levels. Levels above the table no longer
use the old approximate fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:42:52 -04:00
MagicBotandClaude Opus 5.5 db67e56302 Write static_rune_lookup from the loaded race and class caches at world boot
The DB views char_perAccount and dyn_trash_detail lost their race and class
names when 2.11 dropped the Phase 2 tables. The world server now rewrites the
generated static_rune_lookup table (ID, type, name) after the second
Race.loadAllRaces(): 26 player races, 4 base classes, the promotion runes and
the CSR promotion rows 2901-2911. One transaction (delete, batch insert,
commit); on any failure it rolls back and logs a warning, the boot continues.

Names come from the runes: races use obj_name (as in 2.3), classes the rune
names, so DB typos such as "Theif" become "Thief" in the views. No gameplay
change. Needs JSON-Refactor-Plan/sql/2.12-rune-lookup.sql (table, level
functions, views) run before boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:35:58 -04:00
MagicBotandClaude Opus 5.5 8b9aa43c1b Remove the dead skill and power DB loaders
Deletes the uncalled static_rune_skill_adjusts, static_rune_maxskills,
static_skill_skillsgranted, static_skill_skillreq and static_power_powergrant
loaders and their ResultSet constructors. LOAD_RUNE_POWERS stays for the 14
Magister Captain guard contract ids. No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:42:19 -04:00
MagicBotandClaude Opus 5.5 d576ed1468 Stop reading the replaced race, base class and promotion columns
static_rune_race is read only for the race ids and the server-only heights;
the hair/beard style, racebaseclass, baseclass and promotionrunereq queries
are gone. GET_BASE_CLASS is cache-only, which also drops the obj_account
cache-miss query. CSR/admin promotions 2901+ (no promotionrunereq rows) get an
empty allowed list. No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:41:06 -04:00
MagicBotandClaude Opus 5.5 658d52cf41 Remove the dead static_rune_baseeffect path
RuneBaseEffect, its handler, every effectsGranted field and getter (including
the PromotionClass promoID*10+2500..2503 lookups) and the empty
PlayerBonuses.grantEffect had no reader (2.10a). The GameObjectType constant
stays so later ordinals are unchanged. No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:40:12 -04:00
MagicBotandClaude Opus 5.5 64a86152d3 Load rune bases from all non-RACE runes, dropping static_rune_runebase reads
The 315 static_rune_runebase rows are exactly the non-RACE runes, so the loader
selects by rune_type. Removes the server type/subtype columns (no live reader),
the runereq/overwrite path, the GET_RUNEBASE cache-miss query, the runebase
attribute handler, the racerune/baseclassrune loaders and the 111111 checks.
No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:36:34 -04:00
MagicBotandClaude Opus 5.5 13e02707eb Load player rune effects from item_user_power_action instead of static_npc_mobbase_effects
MobBaseEffects.getEffectsForRune builds each rune's list from the RUNE JSON
on first use and caches it: exact repeats are dropped (54 rune files list
their effects twice), the action id resolves to its effect through
PowerActions.cfg, token = SBStringHash(effect id), rank = arg 0,
reqLvl = arg 1. Used by Race, BaseClass, PromotionClass and RuneBase;
MobBase stays on GET_RUNEBASE_EFFECTS until Task 3.4.
Parity (scripts/p210b.py, token level): 1074 runes match. Differences:
- Tireless, Tough as Nails, Taint of Madness: JSON action TRT-* resolves
  to the same effect the DB stores; no change
- CSR 2901-2904, 2906, 2908, 2910, 2911, Non-Aggro Rune 252624: DB token
  was the NoNPCAggro action (no effect found, skipped); now NONPCAGGRO,
  which has no mods; no gameplay change
- Mob-only 12021/12022 NECROPETSTATS and 13236 PEACE are server rows on
  the MobBase path, untouched

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:10:29 -04:00
MagicBotandClaude Opus 5.5 098f5d2c38 Read wpak cfgs from one folder per archive
Files now load from mb.data/wpak/<archive>/<file>, matching MBDATA's
data/wpak layout and the client's Config/Config.wpak naming, so more
archives can sit beside Config/ later. All nine current files are in
wpak/Config/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:38:07 -04:00
MagicBotandClaude Opus 5.5 c63212b900 Remove the old engine.wpak parser package
Replaced by engine.parsers.wpak. Nothing referenced it except the
commented-out calls and the "Loading WPAK data" log line in
ConfigManager, which are removed too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:30:47 -04:00
MagicBotandClaude Opus 5.5 3ae1c72f46 Load Skills, StartingKitTable, CharCreateRuneList and PowActionCostInfo
SkillParser: uid, name, icon, attribute weights and description per
Skills.cfg line (94 skills; the server's ids 1-63 stay in the DB).
StartingKitParser: each kit line is a list of up to 10 item ids under
its RACE= line, as the client reads it; the file's tab columns are not
used (451 kits, same count as static_rune_validkit).
CharCreateRuneParser: race (with sex), class, promotion and attribute
creation rune lists. PowActionCostParser: resource costs per id, the
source of static_effect_costmaps (524 entries). No consumers change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:22:27 -04:00
MagicBotandClaude Opus 5.5 bd145b1e55 Load Powers.cfg at world boot
PowerParser loads Powers.cfg into PowerTemplate._powers. The 22 header
fields are named after the client loader's error messages (0x56E410);
ACTION= lines keep trains, duration, curve and stack fields; prereqs,
curves, flags, messages and CONDITION blocks are kept. Keys compare
case-insensitively, as in the client. Four keys the client ignores
(APPLYDAMAGESELF=, APPLYEFFECTCASTER=, APPLYEFFECTTARGET= and a
POWEREND= typo) are skipped; any other unknown key fails the load.

Numbers are read like the client's atoi/atof with a warning when a
token isn't clean: PRL-033 init time "2.9.0" -> 2.9 and SGE-001
precision "10.0" -> 10, which matches the DB. Ten EQPREREQ lines end
without a required flag, which the client reads as 0.

Needs the fixed Powers.cfg (SRDX-DB header had "SPELL 15 Unknown"
pasted twice; fixed in MBDATA, findings section 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:19:14 -04:00
MagicBotandClaude Opus 5.5 623f7c4ae6 Load Effects.cfg, ModTypes.cfg and CompoundCurves.cfg at world boot
CompoundCurveParser keeps every curve segment (the CompoundCurveType enum
holds one value per curve). ModTypeParser keeps each mod type's Behavior,
which decides the argument layout of a MODS line. EffectParser loads
Effects.cfg into EffectTemplate._effects: header, the four flag keys
(case-insensitive, as in the client), SOURCE lines, every MODS line in
order, and CONDITION lines. Mod layouts follow the client's ArcMod*_Parse
functions; curve names in effects and power actions must exist in
CompoundCurves.cfg. Boot order: curves, mod types, power actions, effects.
No consumers change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:13:26 -04:00
MagicBotandClaude Opus 5.5 6c820940d3 Add wpak cfg reader and PowerActions.cfg loader
New engine.parsers.wpak importer (replaces engine.wpak, which stays unused).
WpakFileReader tokenizes cfg lines (quotes, '#'/'*' comments, trailing '#')
and builds XxxBEGIN/XxxEND blocks; every error names file:line.

PowerActionParser loads PowerActions.cfg into PowerActionTemplate._powerActions,
keyed by action id, at world boot. Header layouts per action type come from
the client's ArcPowerAction*_ParseHeader functions: the trailing 0/1 on
ApplyEffect and friends is the aggressive flag, not a level. effect_ids holds
the action -> effect map needed by Task 2.10b. Three ids (PRE-235..237)
repeat identically; the later entry wins, as in the client. No consumers change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:08:09 -04:00
MagicBotandClaude Opus 5.5 76dcdc0f72 Collapse a rune's skipped non-skill grant warnings into one line
Archon 2018 logged 12 lines at every boot, one per resistance grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:27:34 -04:00
MagicBotandClaude Opus 5.5 4170749a59 Load rune skill reqs from rune_skill_grant instead of static_skill_skillreq
SkillReq.getSkillReqsForRune builds each rune's list from the RUNE JSON
on first use and caches it: skill_value is the grant level and
skill_granted_skills are the prereqs. The prereq amounts (80, Warrior 50,
Sun Dancer 60) are loaded but not yet read; CharacterSkill keeps its
hard-coded 80 and validForWarrior.
Parity (scripts/p29.py): 47 of 79 DB runes match. Value changes (JSON
wins, Task 2.9 decisions):
- Thief Dagger Mastery, Throwing, Unarmed Combat Mastery: level 15 -> 20
- Sentinel Axe/Great Axe/Hammer/Great Hammer/Sword Mastery: 20 -> 15;
  gains Great Sword Mastery (15, Sword); loses Wear Armor, Medium
- Channeler loses Sword; Storm Lord loses Stormcalling
- Blood Horn gains Hammer Mastery (20, Hammer)
- No weapon prereq any more on Archer Archery, Blade Weaver Blade Weaving,
  Sun Dancer Sun Dancing and the 11 Master of X talents
- No effect: Druid/Prelate Sword and Fury Spear 10 -> 1 (promotion is at
  10), discipline/talent level changes (they need level 20), Throwing
  prereqs (always granted), Blade Master Sword prereq (granted at 1)
- JSON-only runes (Archon, Duelist, War Correspondent, Pet, mob/guard
  races) gain lists; Archon resistances and Duelist Dueling are not
  skills and are skipped with a warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:19:38 -04:00
MagicBotandClaude Opus 5.5 d6df01f8f3 Skip mob race runes when loading power granted trains
Their item_power_action entries are mob power ranks by level, not trains,
and getGrantedTrains only looks up a player's race, class, promotion and
runes. RACE runes load only when they are a player Race (static_rune_race).
Cuts the table from 379384 entries / 969 powers to 2596 / 925; no value a
player can read changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:31:49 -04:00
MagicBotandClaude Opus 5.5 7c12d5bde0 Load power granted trains from item_power_action instead of static_power_powergrant
Amount = power_arguments[0] of each power type's first action, keyed by
SBStringHash(power_type). Parity (scripts/p28.py): 69 of the 70 DB runes
match; Wizard 2521 WIZ-064 Conjure Familiar goes from 1 to 20 (decided JSON).
The JSON adds grants on 2189 runes: mob and guard races (2100+), Archon 2018
(ARCHON-001..005 40, CSR-UNINVIS 30), and NPC trainer/guard talents
(252569-252655, 253002). Only Archon is a player race; the talents are not
player runes. The DB loader stays until 2.11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:19:38 -04:00
MagicBotandClaude Opus 5.5 3c57a8bb0e Rename legacy PowerGrant to PowerGrantedTrains
Ends the name clash with the parser's engine.parsers.objects.PowerGrant.
It no longer extends AbstractGameObject: it was never cached or sent as a
game object, and the renamed class has no GameObjectType entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:19:14 -04:00
MagicBotandClaude Opus 5.5 fae15889f7 Load rune powers from item_power_grant instead of static_rune_powers
Parity (scripts/p28.py): all 1066 DB runes with a JSON file match in value
and order; token = SBStringHash(power_type), rank = power_value, name = obj_name.
The 14 Magister Captain guard contract ids (848, 952, 1052, 1182, 1252, 1352,
1452, 1502, 1527, 1552, 1557, 1602, 1652, 980102) have no rune file; guard
spells are server-made, so they keep their DB rows.
The JSON adds 170 class/discipline/talent runes; none is a mobbase, and the
trainer contracts among them never reach the guard contract path in MobAI,
so no reachable value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:18:51 -04:00
MagicBotandClaude Opus 5.5 11cffedd62 Stack a mob's rune skill adjusts by skill type instead of replacing them
applyRuneSkills checked mob.skills for the rune name rather than the skill
type, so every qualifying [rank, level] pair replaced the skill and only
the last one counted. Pairs at or below the mob's level now add up, the
same way trainer caps sum their pairs.

Value changes: only mobbases with several pairs for one skill, Guard Dog
12017 and Guard 13170. Only Guard Dog is in use (contract 910 Personal
Guard Dog); Guard 13170 has no spawns or contracts on dev. Example at
level 50: Dodge and Unarmed Combat 30 -> 110, Unarmed Combat Mastery
20 -> 80. Guard ATR/defense/damage formulas don't read these skills.
No mobbase has a base skill matching a rune skill, so applyMobbaseSkill
values are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:48:53 -04:00
MagicBotandClaude Opus 5.5 865949fd1c Load rune granted skills from level-0 rune_skill_adj instead of static_skill_skillsgranted
Load moved after the rune templates are parsed.
Parity (scripts/p27.py): 76 of 107 DB runes match. Value changes (JSON
wins, Task 2.7 decisions):
- Blade Master 3005: Sword +10 removed (no rune_skill_adj in the JSON;
  Blade Weaver 3006 keeps Sword +10). Reaches players via PlayerBonuses.
- Pet 2522: Dodge and Unarmed Combat 1 -> 22. Not reachable; players
  never hold the Pet rune.
- 29 Master X / trainer runes lose their rows (no level-0 pairs in the
  JSON). Not reachable; players never hold trainer runes. Their caps
  still load into MaxSkills.
- 2243 mob/guard race runes gain rows. Not reachable; PlayerBonuses
  only reads player race/class/promotion/held runes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:46:48 -04:00
MagicBotandClaude Opus 5.5 e3aae0af5c Load trainer max skills from talent rune_skill_adj instead of static_rune_maxskills
Parity (scripts/p27.py): all 78 DB runes match (token, skillLevel = pair
level, maxSkillPercent = pair rank). Talents only, as decided in Task 2.7:
class/discipline/race adjusts are skill grants, and contracts whose class
ID is such a rune (Thief, Huntress, Warlock, Fighter, 8 disciplines,
Vampire) would otherwise gain trainer caps.
The selector adds Brawler 250054 and guard talents 252621-252623; no
contract keys on them, so no reachable value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:46:18 -04:00
MagicBotandClaude Opus 5.5 0921b031ea Load rune skill adjusts from rune_skill_adj instead of static_rune_skill_adjusts
Parity (scripts/p27.py): all 2259 DB runes and 69290 rows match in value
and order; token = SBStringHash(skill_type), name = obj_name.
The JSON adds 95 class/discipline/talent runes; applyRuneSkills reads by
mobbase ID and none of them is a mobbase, so no reachable value change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:45:59 -04:00
MagicBotandClaude Opus 5.5 2798d29d0a Log every refused character creation with the account and IP
All validation refusals in generatePCFromCommitNewCharacterMsg now go
through refuseCreation, which warns with the reason, account and IP. The
negative stat line used to be the only one that named the account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:11:02 -04:00
MagicBotandClaude Opus 5.5 6a4e7f0e1b Refuse creation stats below 1 or above the racial maximum
Follows CharCreate_AdjustAttribute (0xBAC140): a stat must stay at 1 or
more and can't be raised past the racial maximum. Checked after the rune
bonuses and max adjustments but before the rune penalties, since Giant's
Blood lowers the INT/DEX max by 10 but the stat by only 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:10:29 -04:00
MagicBotandClaude Opus 5.5 d6bda1fff0 Cap creation at 10 runes and 8 talents like the client
The rune count limit drops from 16 to the client's 10 slots, race and class
included (CharCreate_ApplyRune 0xBA8C80), and more than 8 talents is
refused (ArcCharacter_AddRune 0x4695D0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:10:03 -04:00
MagicBotandClaude Opus 5.5 bd57c2a3f9 Refuse duplicate creation runes by id and talents sharing a client sub type
Character creation now checks duplicates by rune id instead of relying on
RuneBase object identity, and groups talents by rune_sub_type
(RuneBase.getSubTypeToken) instead of the DB SubType byte. Creation talents
are rank 0, so two in the same group block each other, as in
ArcCharacter_AddRune (0x4695D0).

New groups over the DB (scripts/p26b.py):
- weapon proficiencies 250080-250087: only one at creation
- Blood Gift: Blood of the Dragon, Giant's Blood, Taint of Madness, Taint of Chaos
- Juggernaut + Tough Hide; Tireless + Tough as Nails
- Childhood gains Witch Sight 250112; Dark Whispers and Divine Touch join
  the Background group

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:09:38 -04:00
MagicBotandClaude Opus 5.5 bd04f4d692 Build the non-creation talent set without Set.of for the Java 8 build
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:51:39 -04:00
MagicBotandClaude Opus 5.5 85bc807260 Build the creation talent maps from the runes
AllowedRaceRunesMap and AllowedBaseClassRunesMap now come from TALENT runes
with rune_is_standard_character_creation, using item_race_req and
item_class_req (restrict true = blacklist, false = whitelist) over the
creation race runes (2000-2029) and class runes (2500-2503). Every creation
race and class gets an explicit list, so the 111111 sentinel is no longer
used. static_rune_racerune / baseclassrune loaders stay until 2.11.

Eight flagged runes that are not in CharCreateRuneList.cfg ATTRRUNES are left
out: 2910 Wolfpack Developer, 2911 QA Test Rune, the unused duplicates
250116/250117/250123/250124/250125, and 250081 Proficient with Bows
(commented out in the cfg).

Parity (offline, scripts/p26.py): 84 talents, the same as the DB apart from
these value changes, all decided JSON (2026-10-22):
- 250081 Proficient with Bows: no longer accepted at creation (DB: any race/class)
- 250088 Raised by Barbarians: Elf, Irekei lose it
- 250089 Raised by Centaurs: Elf loses it
- 250092 Raised by Thieves' Guild: Dwarf loses it
- 250093 Raised in the Woods: female Irekei loses it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:49:55 -04:00
MagicBotandClaude Opus 5.5 26aa8de86c Refuse a talent unless it outranks the held one with the same sub type
Test on dev (character Moo): with Enhanced Strength held, the client shaded
an Increased Strength runestone red and showed its refusal popup, but the
server applied it as a downgrade. The client's 0x4768D0 (ArcRuneTemplate
vtable slot 0x1E8) blocks a talent when a held talent with the same
rune_sub_type has an equal or higher rank, rank 0 included. AddRune's
rank-0 replace branch is never reached past that check.

Value changes vs the previous commit:
- Rank-0 stat runes are refused while any rank of their chain is held
  (same as the old DB type 4 block)
- Rank-0 talents in the same group (e.g. Bloodline sub-race runestones)
  are refused instead of replacing each other

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:29:02 -04:00
MagicBotandClaude Opus 5.5 0536aea27a Build rune requirements from the runes and check them like the client
RuneBase(RuneTemplate) fills the race, class and discipline maps from
item_race_req / item_class_req / item_disc_req (names to rune ids through
CObjectIndex; class runes with rune_is_standard_character_creation go to
baseClass, the rest to promotionClass) with a restrict flag for each, and
the level from item_level_req. static_rune_runereq is no longer used for
cached rune bases; GET_RUNE_REQS stays until 2.11.

ApplyRuneMsg follows ArcCharacter_MeetsItemReqs (0x475F40) and
ArcItemRestrict_Check (0x4BAC60): restrict true = blacklist, false =
whitelist. The class check passes when the base class or the promotion is
listed. The discipline check is against the discs already held, using the
new rune's list only.

Parity (offline, scripts/p25a.py with feasible race/promotion pairs under
the client rule; scripts/p25b.py over all 315 rune bases). Value changes,
all decided JSON (D6, 2026-10-21):
- 3001 Animator: + Irekei (4 pairs); Summoner blocks it
- 3008 Commander: Saboteur blocks it
- 3010 Duelist: level 20, 5 races, 7 promotions (had no DB rows; -111 pairs)
- 3018 Enchanter, 3030 Conjurer: + Vampire (4 pairs each)
- 3020 Summoner: - Thief/Warrior/Barbarian (28 pairs), + Centaur Druid/Priest
  (4); Animator blocks it
- 3031 Darksworn: + Irekei Priest (2); 3032 Valkyr: + Human/Aelfborn Warrior (2)
- 3037 Artillerist: + Sentinel (11); 3044 Strigoi: Belgosch blocks it
- 252127 Master of Throwing: - Scout (16 pairs)
- 70 more runes with no DB rows gain their JSON lists (class runes 2500-2526
  except the Pet rune, creation talents and Bloodline talents); only runestones are checked
- Stat chains: the DB "higher rank held" blocks leave the discipline map.
  Ranks 1-8 are still refused by the sub type rule; a rank-0 rune now
  replaces a higher rank as the client does (decided: follow the client)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:02:10 -04:00
MagicBotandClaude Opus 5.5 c3f1861cf7 Replace talents by sub type and rank, cap talents at 8
Follows the client's ArcCharacter_AddRune (0x4695D0, specs/RuneRequirements.md):
a talent replaces a held talent with the same non-zero rune_sub_type when its
rank is 0 or higher than the held rank; otherwise the apply fails. With no
talent to replace, a new talent needs fewer than 8 held talents.

RuneBase gets runeType, a sub type token (SBStringHash(rune_sub_type), or the
stored hash) and rank from the rune. It no longer copies the static_rune_runereq
type 5 overwrite list, and ApplyRuneMsg no longer reads it.

Value changes:
- Stat chains (*MaxIncrease) replace the same ranks as before; the DB typos go
  away (250026 listed 250019 twice and missed 250018)
- Talents in the same group now replace each other when applied from a
  runestone (e.g. Bloodline sub-race talents); before, both were added
- Limit: 8 talents instead of more than 12 runes of any kind
- Rank-0 downgrade (decided: follow the client) takes effect in the next
  commit, when the DB stat-chain blocks leave the discipline map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:00:46 -04:00
MagicBotandClaude Opus 5.5 8f87778c43 Show "You cannot apply that rune" when a rune is refused
applyRune returns false on every failed requirement without telling the
player, so using a rune the character doesn't qualify for did nothing. The
caller now sends popup 161 on refusal, matching popup 160 on success. The id
comes from the server's ErrorPopupMsg list; the popup strings aren't in the
client exe, so it isn't confirmed in IDA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:53:52 -04:00
MagicBotandClaude Opus 5.5 0ff7e7736c Load RuneBase and its attributes from the runes
LoadAllRuneBases builds one RuneBase per static_rune_runebase row from the rune
with the same id (all 315 have one) and fills runeBaseAttributeMap from the
rune; the row now only supplies type, subtype, the runereq maps and effects. A
row without a rune would stay the DB object, without attributes, with a
warning. static_rune_runebaseattribute is no longer read at boot (the
LoadAllAttributes calls are gone from both servers). The SQL loaders stay until
task 2.11.

Value changes (decisions: JSON, D2):
- name: Confesser -> Confessor, Theif -> Thief, LagMaster -> Lagmaster,
  WP Dev -> Wolfpack Developer, Exception X -> Exceptional X (Strength,
  Dexterity, Constitution, Intelligence, Spirit). No code matches these names.
- description: '' / CSR text -> rune_dsc (unread)
- attributes added where the DB had none:
  - admin 2905 Lagmaster spi +200, spi max +200; 2907 Watcher con +200;
    2909 Feature Character every max +200; 2911 QA Test Rune con +200
    (applies to characters granted them; none hold them on dev)
  - creation cost 5/12/10/10/10 on 250116, 250117, 250123-250125 (duplicate
    talents not offered at creation)
  - base classes 2500-2503, Pet 2522, guards 252621-252623: start/max
    attributes (never player CharacterRunes; NPC rune attributes are unread)

All 365 DB attribute rows match the JSON; final parity before the switch
(scripts/p24.py): 0 other differences. The static_rune_runebase comparator is
removed; it would only compare the JSON with itself now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:30:43 -04:00
MagicBotandClaude Opus 5.5 83f57b4def Build RuneBase and its attributes from the rune and compare them
Adds RuneBase(RuneTemplate, RuneBase): name from item_base_name, description
from rune_dsc. type and subtype stay the static_rune_runebase values: they are
server enums that don't map cleanly from rune_type/rune_sub_type (CLASS runes
carry 0/2/3/5/256/289; the creation exclusive-group subtypes 1-10 leave out
Blood Gift, the MaxIncrease chains and the hashed sub types). The requirement
maps and effects are copied from the DB row until 2.5b/2.10.
RuneBaseAttribute.getAttributesForRune builds attribute 0 from
rune_creation_cost, 1-5 from rune_attr_adj, 6-10 from rune_max_attr_adj and
11-15 from item_attr_req. A static_rune_runebase comparator checks name and
attributes against the JSON.

Offline parity (scripts/p24.py, 315 runebase rows + 365 attribute rows vs the
RUNE JSON): every row has a rune; all 365 DB attribute rows match. Differences:
- name: 9 DB typos (Confesser, Theif, LagMaster, WP Dev, Exception Strength/
  Dexterity/Constitution/Intelligence/Spirit)
- attributes: 17 runes with no DB rows gain JSON values: base classes
  2500-2503, Pet 2522, admin 2905/2907/2909/2911, guards 252621-252623, and
  creation cost on 250116, 250117, 250123-250125
- description: '' (or CSR text on 2901-2911) -> rune_dsc; nothing reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:25:20 -04:00
MagicBotandClaude Opus 5.5 98f09d0713 Load Race from the race runes instead of static_rune_race
loadAllRaces builds one Race per static_rune_race row from the RACE rune with
the same id (all 26, including 2018-2021, decision: JSON); the row now only
supplies minHeight and strHeightMod, and a row without a race rune would load
from the DB with a warning. Colors stay in the DB. racebaseclass,
racehairstyle and racebeardstyle are no longer read at boot. The SQL loader
stays until task 2.11.

Value changes (decisions: JSON, D2):
- name drops the ", Male"/", Female" suffix ("Aelfborn, Male" -> "Aelfborn").
  WhoResponseMsg's split(",") and runAfterLoad's suffix strip still work;
  runAfterLoad now resolves 2019 to HalfGiant (it threw on the DB name).
- description '' -> rune_dsc blurb (unread)
- Minotaur hair 6958 -> 6598; Vampire M beard 5020170 -> 5020300
- beard style 0 dropped on Shade M/F, Minotaur, Vampire F (beard styles are
  never validated); hair 0 kept on Aracoix M/F and Shade M/F by the
  empty-hair rule
- 2018 Archon: startingPoints 0 -> 30, token 0 -> -317458791, health 0 -> 20,
  stamina 0 -> 90
- 2019 Half-Giant-Old Man: strMax 150 -> 140, dex 40/65 -> 30/75, con 60/140
  -> 50/130, spi 30/60 -> 35/80, startingPoints 20 -> 15, health/mana/stamina
  0 -> 10/5/70, baseClasses {} -> {Fighter 2500}
- 2020/2021 CSR: all stats 60/120 -> 40/100, startingPoints 0 -> 30, token
  0 -> 52803, health/mana/stamina 0 -> 40/40/10000

All other fields unchanged; final parity before the switch (scripts/p23.py):
22 creation races, 0 differences in attributes, bonuses, startingPoints,
token and base classes. The static_rune_race comparator is removed; it would
only compare the JSON with itself now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:02:06 -04:00
MagicBotandClaude Opus 5.5 a30a065841 Build Race from its race rune and compare every field
Adds Race(RuneTemplate, Race): start/max attributes from rune_attr_adj/
rune_max_attr_adj (min is always 0), health/mana/stamina bonus from
rune_health/mana/stamina, startingPoints = 30 - rune_creation_cost, token =
SBStringHash(rune_sub_type), name from obj_name, description from rune_dsc,
hair/beard styles from rune_hair/rune_beard (style 0 when rune_hair is empty),
and baseClasses = the creation class runes whose item_race_req names the race
sub type. minHeight/strHeightMod come from the static_rune_race row; colors,
skills, powers and effects still load from the DB by rune id. A
static_rune_race comparator checks every field against a JSON-built Race.

Offline parity (scripts/p23.py, 26 rows + racebaseclass/hairstyle/beardstyle
vs the RACE runes): the 22 creation races match in every attribute, bonus,
startingPoints, token and base class. Differences:
- name: "Aelfborn, Male" -> "Aelfborn" on every row
- hair 0 dropped by the JSON on Aracoix M/F and Shade M/F (kept by the
  empty-hair rule); beard 0 dropped on Shade M/F, Minotaur, Vampire F
- Minotaur hair 6958 -> 6598, Vampire M beard 5020170 -> 5020300
- 2018 Archon: startingPoints 0 -> 30, token 0 -> SBStringHash("Celestial"),
  health 0 -> 20, stamina 0 -> 90
- 2019 Half-Giant-Old Man: str/dex/con/spi start and max, startingPoints
  20 -> 15, health/mana/stamina 0 -> 10/5/70, baseClasses {} -> {Fighter}
- 2020/2021 CSR: stats 60/120 -> 40/100, startingPoints 0 -> 30, token 0 ->
  SBStringHash("CSR"), health/mana/stamina 0 -> 40/40/10000

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:01:43 -04:00
MagicBotandClaude Opus 5.5 957293165b Load PromotionClass from the promotion runes instead of static_rune_promotion
LoadAllPromotions builds one PromotionClass per promotion CLASS rune (22,
2504-2526 without 2522) and caches it, then runs the DB loader, which skips
cached ids, so only the CSR/admin rows 2901-2911 still come from
static_rune_promotion (decision: DB fallback). The world server calls it in
place of GET_ALL_PROMOTIONS; the login server now preloads promotions too, so
character load there gets the same cached objects. The SQL loader stays until
task 2.11.

Value changes: Templar 2516 manaMod 14 -> 15 (decision: JSON, D2).
description NULL -> rune_dsc blurb (unread). All other fields unchanged;
final parity before the switch (scripts/p22.py): 22 promotion rows, 0
differences in name, token, health, stamina and the 163 allowed rune tokens.
The static_rune_promotion comparator is removed; it would only compare the
JSON with itself now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:33:45 -04:00
MagicBotandClaude Opus 5.5 e91c803a32 Build PromotionClass from its class rune and compare every field
Adds PromotionClass(RuneTemplate): health/mana/stamina from rune_health/mana/
stamina, token = SBStringHash(rune_sub_type), description from rune_dsc, and
allowedRunes = the sub type tokens of the race and base class runes named in
item_race_req/item_class_req (all whitelists). Skills, powers and effects still
load from the DB by rune id (tasks 2.8, 2.9, 2.10). isPromotionRune selects the
non-creation CLASS runes that require a base class (skips 2500-2503 and Pet).
A static_rune_promotion comparator checks every field against a JSON-built
PromotionClass; rows 2901+ (TALENT runes in the JSON) report as missing.

Offline parity (scripts/p22.py, 33 rows + 163 promotionrunereq rows vs 22
promotion runes): 0 differences in name, token, health, stamina and allowed
rune tokens. Templar 2516 manaMod is 14 in the DB and 15 in the JSON.
2901-2911 have no promotion rune. description is NULL in the DB and the
rune_dsc blurb in the JSON; nothing reads PromotionClass.getDescription().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:33:08 -04:00
MagicBotandClaude Opus 5.5 992177eb0c Return the cached PromotionClass from GET_PROMOTION_CLASS
Character load and the promotion trainer re-read static_rune_promotion on every
call and replaced the cached object. They now get the object loaded at boot,
as GET_BASE_CLASS already does; the DB query is only a miss fallback.
No value changes: the cached and re-read objects hold the same row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:32:24 -04:00
MagicBotandClaude Opus 5.5 c8c322a3f5 Load BaseClass from the class runes instead of static_rune_baseclass
LoadAllBaseClasses builds one BaseClass per CLASS rune with
rune_is_standard_character_creation (2500-2503; promotions and Pet are
skipped) and caches it. Race.baseClasses picks up the cached objects through
GET_BASECLASS_FOR_RACE. The SQL loader stays until task 2.11.

Value changes: description goes from '' to the rune_dsc class blurb (unread).
All other fields unchanged; final parity before the switch (scripts/p21.py):
4 rows, 0 differences in name, attribute mods, token, health/mana/stamina mods.
The static_rune_baseclass comparator is removed; it would only compare the
JSON with itself now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:55:09 -04:00
MagicBotandClaude Opus 5.5 5acd3492ed Build BaseClass from its class rune and compare every field
Adds BaseClass(RuneTemplate): attributes from rune_attr_adj, health/mana/stamina
from rune_health/mana/stamina, token = SBStringHash(rune_sub_type), description
from rune_dsc. Skills, powers and effects still load from the DB by rune id
(tasks 2.8, 2.9, 2.10). The static_rune_baseclass comparator now checks every
field against a JSON-built BaseClass.

Offline parity (scripts/p21.py, 4 rows vs the CLASS runes with
rune_is_standard_character_creation = 2500-2503): 0 differences except
description, which is '' in the DB and the class blurb in rune_dsc. Nothing
reads BaseClass.getDescription().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:54:46 -04:00
MagicBotandClaude Opus 5.5 0283f85e87 Remove the legacy ItemTemplate and static_item_templates loader
Every reader moved to CObjectTemplate in 1.7, so the DB-loaded
engine.objects.ItemTemplate, LOAD_ITEM_TEMPLATES and linkCacheTemplates
are dead. No value changes: nothing read the legacy maps any more.
The boot log loses the legacy per-type count and the link warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:53:14 -04:00
MagicBotandClaude Opus 5.5 4b8430b0ba Use GameObjectType to check the ./fill target
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:44:11 -04:00
MagicBotandClaude Opus 5.5 f07f9945d7 Compare the selected warehouse by UUID in ./fill
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:42:29 -04:00
MagicBotandClaude Opus 5.5 077dcc071d Rename ./fillwarehouse to ./fill and require a selected warehouse
The command now acts on the selected building and refuses anything
whose blueprint is not a warehouse, or a warehouse that is not its
city's active one, instead of using the city the player stands in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:42:09 -04:00
MagicBotandClaude Opus 5.5 c616afa33b Add ./fillwarehouse dev command
Sets every resource in the warehouse of the city the player stands in,
either to an optional amount or, by default, to each resource's
deposit limit. Makes forge and work order testing practical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:40:01 -04:00
MagicBotandClaude Opus 5.5 1ac522a2e0 Fix /makeitem failing to persist items
MakeItemCmd left the container at NONE, which item_CREATE's cont
parameter rejects, so PERSIST returned null and addItemToInventory
threw a NullPointerException. Set the container to INVENTORY as
CreateItemCmd does, and report an error if PERSIST still fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:27:47 -04:00
MagicBotandClaude Opus 5.5 566870de79 Move the remaining legacy item template lookups to CObjectTemplate
Dev commands, BuildingManager, ChatManager, ForgeManager, NPCManager,
NPC, Warehouse, WorkOrder and ManageNPCMsg now call
CObjectTemplate.lookup instead of ItemTemplate.templates.get. Forge
production cost reads getItemResourceCosts(). Only the DB loader and
linkCacheTemplates still use the legacy class (removed in Task 1.8).

Removes the static_item_templates ParityCheck comparator. Final parity
(offline p17.py, 7586 rows): every id has a cache template, 0 item_type
differences, 0 item_resource_costs differences (84 ITEMs have costs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:56:08 -04:00
MagicBotandClaude Opus 5.5 6b4e9815e0 Switch Item.template and MobLoot to CObjectTemplate
Item.template is now the cache template from CObjectTemplate.lookup
instead of the legacy DB ItemTemplate, so item.template.cacheTemplate.X
becomes item.template.X. MobLoot, LootManager, ItemManager.isTwoHanded/
isShield and CombatManager.getSwingAnimation take a CObjectTemplate.

No value changes: every getter already read the cache template through
the bridge, and the lookup resolves the same 7586 ids as
static_item_templates with the same item_type (offline parity p17.py,
0 differences).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:55:28 -04:00
MagicBotandClaude Opus 5.5 f39e35ac23 Apply the dual-wield damage cut only when both hands hold weapons
The 30% cut never applied: each hand was passed itself as the other
hand, and the check compared the template to ItemType.WEAPON. The
client (0x466EB0 / 0x46D200) multiplies min and max damage by 0.7 when
both held slots contain an item_type WEAPON. Value change: players
wielding two weapons now deal 70% damage per hand; single weapon,
weapon + shield and unarmed are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:54:02 -04:00
MagicBotandClaude Opus 5.5 70ea9b3bc9 Move the vendor mod table off the legacy item template
LOAD_TEMPLATE_MODTABLES now returns a templateID -> modTable map that
ForgeManager owns (vendorModTables). ForgeManager.forgeItem, WorkOrder
validation and the ManageNPCMsg forge item list read it through
ForgeManager.getVendorModTable(), and ItemTemplate.modTable is removed.

Values are unchanged: static_vendor_items has 5089 rows, one per
templateID, all present in static_item_templates, modTable 0-30.
Templates not in the table still read 0. The old loader stopped at the
first templateID missing from ItemTemplate.templates; there are none.
No comparator added or removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:37:54 -04:00
MagicBotandClaude Opus 5.5 2b4c1a73d1 Read item user power actions from the cache template
The magical name color check and loadTemplateEnchantments read
getItemUserPowerAction(). The list keeps duplicates, so the baked-in
enchantments collapse a repeated power to its last entry, as the legacy
HashMap did; applying it twice would add its name to effectNames twice and
double its share of calcMagicValue.

Offline parity (p15e.py, 7586 rows): 0 differences in the lists (power and
arguments, in order). Duplicates exist only on 81 RUNE templates (mob,
guard and siege runes, plus discipline rune 3006 Blade Weaver with two
identical AMBIDEXTERITY entries); none on ITEM or DEED.

Value changes: none. Enchantments are now applied in file order instead
of HashMap order; effects are keyed by effect id and calcMagicValue is a
sum, so the result is the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:35:38 -04:00
MagicBotandClaude Opus 5.5 a37de9fc31 Read the item power grant from the cache template
The BUCKET/POTION/SCROLL use path takes the first item_power_grant entry
in file order from getItemPowerGrant() instead of the first key of the
legacy HashMap.

Offline parity (p15e.py, 7586 rows): 0 differences in the grant lists.
The legacy HashMap order picks a different power on 664 templates, all
RUNE folder runes with item_type RUNE, which take the RUNE case and never
reach this path. Every ITEM-folder grant (scrolls, potions, bucket, rod,
gift rings) has exactly one entry and no DEED has one, so the power and
value used are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:35:19 -04:00
MagicBotandClaude Opus 5.5 81d4ab7b99 Read item skill used and mastery from the cache template
CombatManager shield checks, weapon skill/mastery lookups in
AbstractCharacter, armor defense and PlayerCharacter.validEquip read
getItemSkillUsed/getItemSkillMasteryUsed. Offline parity (p15d.py, 7586
rows): 0 mastery differences; skill used differs only where the legacy
loader blanked it.

The cache keeps "Cloth" (226 ARMOR templates), which the legacy loader
turned into "". Cloth is a placeholder in Skills.cfg that no character
has, so getArmorDefense now checks for it explicitly and still returns the
flat defense rating. No weapon uses Cloth and no power equip prereq names
it, so the other sites behave the same.

Value changes:
- 195340 Fortified Dragon Plate Helm of the Lion: skill used is the
  unresolved hash "2624663" instead of "". Defense is still the flat
  rating (no such skill), but players wearing it now hit the
  "armor equipped without the necessary skill" error log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:29:01 -04:00
MagicBotandClaude Opus 5.5 7abc63ffc3 Read item skill requirements from the cache template
validForSkills, isShield, the guard attack rating and the NPC vendor skill
filter read getItemSkillReq. Offline parity (p15d.py, 7586 rows): 0
differences. No template lists "Cloth" in item_skill_req and none has more
than one entry, so file order vs HashMap order is moot.

Value changes: none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:28:32 -04:00
MagicBotandClaude Opus 5.5 5b3c511b55 Read item eq slots from the cache template
isTwoHanded, validForSlot, getSwingAnimation and the mob equipment set
read getItemEqSlotsOr/And, which drop the exporter's NONE. Offline parity
(p15c.py, 7586 rows): 0 differences once NONE is ignored.

Value changes:
- validForSlot: items whose only slot is NONE no longer pass for slot NONE.
- getSwingAnimation: the 7 slotless weapons (Rock 19297, Arrow 25650,
  siege ammo 29000/29010/29020/29030/29350) return the default 75 instead
  of indexing their empty left-hand anim list.
- MobBase equipment sets skip slotless items instead of keying them under
  NONE. No EQUIP booty entry references one (the 104 sets that do are ITEM
  booty sets).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:23:41 -04:00
MagicBotandClaude Opus 5.5 73947f3dc8 Drop NONE from rune and deed eq slots
Matches ItemParser, so every CObjectTemplate returns an empty slot set for
the exporter's ["NONE"] mask. All 2695 RUNE and 880 DEED templates have
["NONE"] only; nothing reads these sets yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:22:57 -04:00
MagicBotandClaude Opus 5.5 c7d8483d55 Read item race/class/disc requirements from the cache template
ValidRace/ValidClass/ValidDiscipline use the single req set plus the
restrict flag (true = blacklist, false = whitelist) instead of the legacy
req/res split. Offline parity (p15c.py, 7586 rows): 0 differences in
restrict or set for race, class and disc.

Value changes: none for equip checks. The legacy disc blacklist branch was
inverted (passed only when a listed disc was held); the new check passes
when none is held. No ITEM has a disc blacklist (only 19 RUNEs), and
ValidDiscipline has no callers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:22:36 -04:00
MagicBotandClaude Opus 5.5 0688cf231f Fix isTwoHanded always returning false
EnumSet.contains(EnumSet) is never true; use containsAll so the 350
weapons with item_eq_slots_and {LHELD, RHELD} block the other hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:22:09 -04:00
MagicBotandClaude Opus 5.5 6b2f000434 Read the weapon block from the cache template item_weapon
Range, speed, damage list and swing anims now come from
engine.parsers.objects.ItemTemplate.getWeapon(templateID). Non-weapons and
missing templates get an empty block, matching the legacy zero defaults.

Offline parity (scripts/p15b.py, 811 WEAPON rows vs COBJECTS/ITEM): 0
differences in speed, range, projectile, idle anim, damage list or either
anim list. No duplicate damage types exist, so sum vs first is moot; first
entry is used as before. 26 weapons (ammo, lutes, no-stats crossbows) have an
empty damage list, same as the DB. No value changes. No comparator removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:17:24 -04:00
MagicBotandClaude Opus 5.5 441a935bd9 Read armor resists from the cache template EnumMap
Resists.getArmorResists and NPCManager.applyEquipmentResists now read
cacheTemplate.getCombatAttackResist() with DamageType keys instead of the
legacy String-keyed map, and default a missing key to 0 instead of an
unboxing NPE.

Offline parity (scripts/p15a.py, all 7586 static_item_templates rows vs
the template CObjectIndex.lookup picks): 0 differences in the full
combat_attack_resist map. Every ITEM/DEED/RUNE template carries all 16
keys, so no value changes. No comparator added or removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:11:21 -04:00
MagicBotandClaude Opus 5.5 b63826a7b7 Import DeedTemplate in PlaceAssetMsgHandler
c7747467 used DeedTemplate._deeds in the deed placement check without
importing it, which broke the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:52:22 -04:00
MagicBotandClaude Opus 5.5 2774d8a510 Read offering info and bane rank through the bridge
Shrine boons read getItemOfferingInfo() and the banestone and bane
production paths read getItemBaneRank() on template.cacheTemplate.

The legacy parser only kept item_bane_rank when ACTIONRESPONSE is
4250517122; the cache parsers always read it. Parity shows this makes
no difference: every template outside that gate has bane rank 0.

Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_offering_info and item_bane_rank. No value changes expected.

JSON-Refactor-Plan Task 1.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:46:14 -04:00
MagicBotandClaude Opus 5.5 9a10574e59 Read defense, bulk and primary attr from the cache ItemTemplate
item_defense_rating, item_bulk_factor and item_primary_attr are
Item-only fields, not on CObjectTemplate, so their consumers read
engine.parsers.objects.ItemTemplate._items by templateID, like the
RuneTemplate and DeedTemplate consumers. The primary attr checks
compare with == because the cache template leaves the attr null
(legacy: None) on non-weapons.

Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_defense_rating, item_bulk_factor and item_primary_attr. Every DB
ARMOR and WEAPON template is in the ITEM folder, so both sides gate
these fields on the same templates. No value changes expected.

JSON-Refactor-Plan Task 1.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:45:59 -04:00
MagicBotandClaude Opus 5.5 d197f62c1f Read template health, charges and flags through the bridge
Consumers of the legacy ItemTemplate combat_health_full,
item_initial_charges and item_flags now read getCombatHealthFull(),
getItemInitialCharges() and getItemFlags() on template.cacheTemplate.
No consumer read the template's combat_health_current; the
combat_health_current reads are the Item's own field.

Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
combat_health_current, combat_health_full, item_initial_charges and
item_flags ("None" dropped on both sides). No value changes expected.

JSON-Refactor-Plan Task 1.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:45:17 -04:00
MagicBotandClaude Opus 5.5 dc4a9ee08a Read item weight, value and base name through the bridge
Consumers of the legacy ItemTemplate item_wt, item_value and
item_base_name now read getItemWt(), getItemValue() and
getItemBaseName() on template.cacheTemplate.

Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_wt, item_value and item_base_name. No value changes expected.

JSON-Refactor-Plan Task 1.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:44:35 -04:00
MagicBotandClaude Opus 5.5 b8ac8cce3e Read item_type through the cache template bridge
Every consumer of the legacy ItemTemplate item_type now reads
template.cacheTemplate.getItemType(). Left on the legacy field: the
type count in dbItemHandler.LOAD_ITEM_TEMPLATES, which runs before the
bridge is linked. The charter checks already read DeedTemplate.

Parity (offline, static_item_templates vs the template
CObjectIndex.lookup picks, scripts/p14.py, 7586 templates: ITEM 4011,
DEED 880, RUNE 2695): 0 differences in item_type. No value changes
expected.

JSON-Refactor-Plan Task 1.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:43:57 -04:00
MagicBotandClaude Opus 5.5 6825f94f1e Check charters through DeedTemplate
GuildCreationFinalizeHandler and UseCharterMsg now match charters by
DeedTemplate._deeds instead of the shared item_type getter, like the
other deed paths.

Value change: template 545 "Charter" (ITEM folder, CHARTER) no longer
counts as a charter. It is a base template that players can't get: its
only DB reference is static_vendor_items with vendorType 0, it has no
loot rows, and it is not in GuildCharterType.templateLookup, so guild
creation already rejected it.

JSON-Refactor-Plan Task 1.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:47:09 -04:00
MagicBotandClaude Opus 5.5 c774746736 Read deed consumers from DeedTemplate
Deed placement (ObjectActionMsgHandler, PlaceAssetMsgHandler) reads
deed_structure_id from DeedTemplate._deeds. The deed, employment
contract and charter type checks (PlaceAssetMsgHandler,
ActivateNPCMsgHandler, GuildCreationFinalizeHandler, UseCharterMsg,
Item.serialize) read item_type through the cache template bridge.
Charters go through the shared getter rather than _deeds because
template 545 "Charter" is a CHARTER in the ITEM folder.

Parity (offline, static_item_templates vs COBJECTS, scripts/p13.py,
881 templates: all 880 DEED plus ITEM 545): 0 differences in
item_type and deed_structure_id. Every DB DEED, EMPLOYMENTCONTRACT,
PETTOTEM and FURNITUREDEED id is in the DEED folder. No value changes
expected. No pet totem or furniture deed consumer reads template
fields, so neither changes.

JSON-Refactor-Plan Task 1.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:42:54 -04:00
MagicBotandClaude Opus 5.5 3e087bd1ac Read mob rune damage and natural attacks from RuneTemplate
AbstractCharacter mob damage/ATR and CombatManager.getNaturalAttack now
read RuneTemplate._runes instead of the legacy ItemTemplate map.

Parity (offline, static_item_templates vs COBJECTS/RUNE, 2695 runes):
0 differences in rune_min_damage, rune_max_damage, rune_attack and
rune_naturalattacks. All 2366 runes with natural attacks use the
10-value layout. No value changes expected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:35:02 -04:00
MagicBotandClaude Opus 5.5 765cb9edb6 Bridge legacy ItemTemplate to its cache template
Each engine.objects.ItemTemplate gets a cacheTemplate field, filled by
linkCacheTemplates() right after CObjectIndex.build() on both servers,
so consumers can move to CObjectTemplate one field at a time.

ParityCheck gains a static_item_templates comparator: a null bridge is
reported as missing in json, and obj_name and item_type are compared
through the interface to catch a wrong folder pick.

Import convention until the legacy class is removed (Task 1.8): the
legacy engine.objects.ItemTemplate keeps the plain import; the cache
class is always written engine.parsers.objects.ItemTemplate. Migrated
consumers should depend on CObjectTemplate instead.

JSON-Refactor-Plan Task 1.1. No consumers change; no values change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:21:47 -04:00
MagicBotandClaude Opus 5.5 4765e7993f Add shared obj/combat/item getters to CObjectTemplate
The ITEM, DEED and RUNE cache templates implement the getters for the
65 fields all three define. The Item-only fields (weapon, primary and
secondary attr, bulk, defense rating, sheath, stub flag/id) stay off the
interface. CObjectTemplate.lookup delegates to CObjectIndex.lookup.

JSON-Refactor-Plan Task 1.1 (D1). No consumers change; no values change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:20:33 -04:00
MagicBotandClaude Opus 5.5 6ef987b7c6 Block all non-siege building placement while the city has a bane
Moves the warehouse-only check into validateBuildingPlacement, which every
city asset placement except siege equipment and trees passes through.
Matches client error 74, "Only buildings of war may be placed during a bane".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:15:54 -04:00
MagicBotandClaude Opus 5.5 9db194889f Use client error 74 for warehouse placement during a bane
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:14:37 -04:00
MagicBotandClaude Opus 5.5 c97b4d7c3d Declare the bane local at the top of placeWarehouse
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:12:56 -04:00
MagicBotandClaude Opus 5.5 e450224902 Block warehouse placement while the city has a bane
Placing a new warehouse discards the destroyed warehouse's rubble loot,
so defenders could deny attackers the loot by rebuilding mid-siege.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:09:31 -04:00
MagicBotandClaude Opus 5.5 875809efe0 Look up resource templates at loot time instead of caching them on ResourceType
ResourceType.template was filled in the enum constructor, which can run
before ItemTemplate.templates is loaded, leaving it null. lootBuilding was
its only reader and threw on the first non-gold resource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:38:55 -04:00
MagicBotandClaude Opus 5.5 ef6fe4aa5b Skip rubble when locating a city's warehouse building at load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:21:57 -04:00
MagicBotandClaude Opus 5.5 145bb15232 Skip the warehouse delete when the owner destroys warehouse rubble
city.warehouse is already null after a combat destruction, which threw in
DELETE_WAREHOUSE. If a new warehouse was placed it would have deleted that
one instead, so only a live (rank != -1) warehouse clears city.warehouse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:20:21 -04:00
MagicBotandClaude Opus 5.5 e64d64bf18 Keep destroyed warehouse contents in memory so the rubble can be looted
destroyOrDerank nulled city.warehouse on destruction, so the loot click
found no warehouse and returned silently. The destroyed Warehouse is now
held in Warehouse.destroyedWarehouses keyed by the rubble building UUID
until it is looted empty, the rubble is destroyed, or the city places a
new warehouse. Contents are not persisted across a reboot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:19:40 -04:00
MagicBotandClaude Opus 5.5 0865a7be74 Log only the JSON error and workorder ID when a workorder fails to load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:24:04 -04:00
MagicBotandClaude Opus 5.5 a079deef8d Log only the JSON error and city UUID when a warehouse fails to load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:23:18 -04:00
MagicBotandClaude Opus 5.5 963130565e Make MB_MINION_SPAWN_TIME a required ConfigManager entry
0 or blank keeps the normal guard/siege minion spawn timers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:00:53 -04:00
MagicBotandClaude Opus 5.5 becab380d2 Add optional MB_MINION_SPAWN_TIME override for guard and siege minion spawns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 07:41:16 -04:00
MagicBotandClaude Opus 5.5 5c78bc7fa5 Skip the shopkeeper rune when applying an NPC rune set
applyRunesForNPC already adds 252620, and most rune sets repeat it, so
NPCs with a set listed the shopkeeper rune twice after the runeSet fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:30:18 -04:00
MagicBotandClaude Opus 5.5 4e30a04619 Fix /makeitem looking up the template by string instead of template id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:06:10 -04:00
MagicBotandClaude Opus 5.5 42a1c8a532 Fix Warehouse.loot looking up the template by enum instead of template id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:06:10 -04:00
MagicBotandClaude Opus 5.5 2a2c528981 Fix NPC rune sets storing the set id instead of the rune id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:06:10 -04:00
MagicBotandClaude Opus 5.5 58e0fa32fb Add dev-only DB/JSON parity check at world boot
ParityCheck compares DB-built objects with the cache JSON and logs
table/id/field differences plus a per-table summary. It runs only when
the optional env var MB_PARITY_CHECK=true is set; it is not added to the
ConfigManager enum because init() aborts on any missing enum entry.

First comparator: static_rune_baseclass name vs RUNE obj_name.
No gameplay values change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:51:22 -04:00
MagicBotandClaude Opus 5.5 62b4a2a897 Skip cache JSON files whose name is not a 32-bit template id
The client cache holds RUNE/10788957847552 (2512 << 32), which would
overflow Integer.parseInt and abort the rune load. Such files are now
skipped with a warning. The file is not in MBDATA yet, so the
880/2695/4011 counts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:44:45 -04:00
MagicBotandClaude Opus 5.5 d9d1c41d9e Add CObjectIndex lookups over the cache templates
Built at login and world bootstrap after the three parsers load (Task 0.1):
- lookup(id) across ITEM, then DEED, then RUNE
- race (MonsterType) -> creation race rune ids, male and female
- ClassType -> class rune id, DisciplineType -> discipline rune id
- SBStringHash(rune_sub_type) token -> runes
- skill and power name <-> SBStringHash token, from every template field
  that names one

No consumers yet; no values change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:33:35 -04:00
MagicBotandClaude Opus 5.5 cb427de440 Add CObjectTemplate marker interface for cache templates
ITEM, DEED and RUNE templates share one id space (JSON-Refactor-Plan D1).
Empty for now; the shared getters come with Task 1.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:33:34 -04:00
MagicBotandClaude Opus 5.5 74828c620d Load item cache JSON at login and world server bootstrap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:51:20 -04:00
MagicBotandClaude Opus 5.5 fbd38732dc Add ITEM cache JSON parser
ItemTemplate follows ArcItemTemplate_Load (sb.exe 0x4AABD0): weapon and
armor blocks keyed by item type, sheath and stub blocks, and empty equip
slot masks read as empty sets. Adds SheathSlot and readHashLabel, which
keeps unresolved hashes such as item_skill_used in 195340.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:51:20 -04:00
MagicBot 75e10f613f Reporting within parser 2026-10-17 08:46:23 -04:00
MagicBot 783d2d4dc9 Bootstrap count reporting for DEED/RUNE 2026-10-17 08:40:37 -04:00
MagicBot 3870b283d8 Bootstrap count reporting for DEED/RUNE 2026-10-17 08:39:48 -04:00
MagicBotandClaude Opus 5.5 542ae79e2c Load deed and rune cache JSON at login server bootstrap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:49:09 -04:00
MagicBotandClaude Opus 5.5 08d4bc2501 Treat unknown enum values in deed and rune JSON as parse failures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:48:11 -04:00
MagicBotandClaude Opus 5.5 7309159f83 Abort world server startup when any deed or rune file fails to parse
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:45:24 -04:00
MagicBotandClaude Opus 5.5 ad476db166 Abort world server startup when deed or rune templates fail to load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:44:42 -04:00
MagicBotandClaude Opus 5.5 a75cbb5cc1 Load deed and rune cache JSON at world server bootstrap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:43:48 -04:00
MagicBotandClaude Opus 5.5 2bee93087d Add static template maps to DeedTemplate and RuneTemplate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 07:42:00 -04:00
MagicBotandClaude Opus 5.5 99a4a6741a Remove unused StringUtils.hashString and FastMath.hash
Neither matches the client string hash; Hasher.SBStringHash does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:52:46 -04:00
MagicBotandClaude Opus 5.5 1ac5a248db Read named grant keys and signed values from new COBJECTS JSON
Grant pair lists now carry name strings ("HLR-021", "Unarmed Combat")
instead of integer hashes, so they are stored as name -> value maps.
Unresolved hashes are kept as their decimal string and logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:46:37 -04:00
MagicBotandClaude Opus 5.5 b56d4ee243 Note lossy power action maps in ItemTemplate
item_user_power_action and item_power_action are keyed by power, so
rune step tables (repeated powers, multiple power_actions entries) are
collapsed to one entry. Nothing reads the rune data today; the new
RuneTemplate keeps the full lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:21:41 -04:00
MagicBotandClaude Opus 5.5 0fb1c078ad Normalize case and punctuation in DamageType.getDamageType
The validity check compared names case-sensitively, so mixed-case
input such as "Fire" or "Crushing" from static_power_failcondition and
static_power_poweraction, and SourceType names in the fortitude check,
all resolved to NONE. Input is now stripped of spaces, commas and
hyphens and upper-cased before lookup, so "Anti-Siege" maps to
ANTISIEGE as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:18:38 -04:00
MagicBotandClaude Opus 5.5 c14231afb9 Add RuneType enum and DamageType.ANTISIEGE
RuneType covers the rune_type values in the client cache data
(CLASS, DISCIPLINE, RACE, TALENT). ANTISIEGE matches the
combat_attack_resist key in the cache data.

RuneTemplate.rune_type and combat_attack_resist on both cache
templates now use these enums.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:16:05 -04:00
MagicBotandClaude Opus 5.5 0d48080e38 Add cache parsers and POJOs for DEED and RUNE templates
New engine.parsers package:
- cache: DeedParser and RuneParser load COBJECTS/DEED and COBJECTS/RUNE
  JSON straight from the data dir, keyed by file id. CacheParserUtil
  holds the shared JSON helpers.
- objects: standalone DeedTemplate and RuneTemplate covering every
  field in the client data, plus shared PowerGrant/UserPowerAction.
- wpak: placeholder for upcoming wpak parsers.

Not wired into server startup yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:09:41 -04:00
316 changed files with 12194 additions and 7143 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;
@@ -118,11 +118,6 @@ public class WorldGrid {
return outbound;
}
public static HashSet<AbstractWorldObject> getObjectsInRangePartialNecroPets(Vector3fImmutable loc, double r) {
HashSet<AbstractWorldObject> outbound = new HashSet<>();
return outbound;
}
public static HashSet<AbstractWorldObject> getObjectsInRangeContains(Vector3fImmutable loc, double r, int mask) {
HashSet<AbstractWorldObject> outbound = getObjectsInRangePartial(loc, r, mask);
return outbound;
+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
+1 -60
View File
@@ -13,13 +13,7 @@ import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.mbEnums.GameObjectType;
import engine.objects.BaseClass;
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 class dbBaseClassHandler extends dbHandlerBase {
@@ -33,59 +27,6 @@ public class dbBaseClassHandler extends dbHandlerBase {
if (id == 0)
return null;
BaseClass baseClass = (BaseClass) DbManager.getFromCache(GameObjectType.BaseClass, id);
if (baseClass != null)
return baseClass;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `obj_account` WHERE `UID`=?")) {
preparedStatement.setLong(1, id);
ResultSet rs = preparedStatement.executeQuery();
baseClass = (BaseClass) getObjectFromRs(rs);
} catch (SQLException e) {
Logger.error(e);
}
return baseClass;
}
public ArrayList<BaseClass> GET_BASECLASS_FOR_RACE(final int id) {
ArrayList<BaseClass> baseClasses = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT b.* FROM `static_rune_baseclass` b, `static_rune_racebaseclass` r WHERE b.`ID` = r.`BaseClassID` && r.`RaceID` = ?")) {
preparedStatement.setInt(1, id);
ResultSet rs = preparedStatement.executeQuery();
baseClasses = getObjectsFromRs(rs, 20);
} catch (SQLException e) {
Logger.error(e);
}
return baseClasses;
}
public ArrayList<BaseClass> GET_ALL_BASE_CLASSES() {
ArrayList<BaseClass> baseClasses = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_baseclass`;")) {
ResultSet rs = preparedStatement.executeQuery();
baseClasses = getObjectsFromRs(rs, 20);
} catch (SQLException e) {
Logger.error(e);
}
return baseClasses;
return (BaseClass) DbManager.getFromCache(GameObjectType.BaseClass, id);
}
}
+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` = ?")) {
@@ -1,303 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.gameManager.PowersManager;
import engine.mbEnums;
import engine.powers.EffectsBase;
import engine.powers.effectmodifiers.*;
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;
import java.util.HashSet;
public class dbEffectsBaseHandler extends dbHandlerBase {
public dbEffectsBaseHandler() {
}
public static ArrayList<EffectsBase> getAllEffectsBase() {
ArrayList<EffectsBase> effectList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement prepareStatement = connection.prepareStatement("SELECT * FROM static_power_effectbase ORDER BY `IDString` DESC")) {
ResultSet rs = prepareStatement.executeQuery();
while (rs.next()) {
EffectsBase effectBase = new EffectsBase(rs);
effectList.add(effectBase);
}
} catch (SQLException e) {
Logger.error(e.toString());
}
return effectList;
}
public static void cacheAllEffectModifiers() {
String IDString;
AbstractEffectModifier abstractEffectModifier = null;
try (Connection connection = DbManager.getConnection();
PreparedStatement prepareStatement = connection.prepareStatement("SELECT * FROM static_power_effectmod")) {
ResultSet rs = prepareStatement.executeQuery();
while (rs.next()) {
IDString = rs.getString("IDString");
EffectsBase effectBase = PowersManager.getEffectByIDString(IDString);
mbEnums.ModType modifier = mbEnums.ModType.GetModType(rs.getString("modType"));
//combine item prefix and suffix effect modifiers
abstractEffectModifier = getCombinedModifiers(abstractEffectModifier, rs, effectBase, modifier);
if (abstractEffectModifier != null) {
if (EffectsBase.modifiersMap.containsKey(effectBase.getIDString()) == false)
EffectsBase.modifiersMap.put(effectBase.getIDString(), new HashSet<>());
EffectsBase.modifiersMap.get(effectBase.getIDString()).add(abstractEffectModifier);
}
}
} catch (Exception e) {
Logger.error(e);
}
}
private static AbstractEffectModifier getCombinedModifiers(AbstractEffectModifier abstractEffectModifier, ResultSet rs, EffectsBase effectBase, mbEnums.ModType modifier) throws SQLException {
switch (modifier) {
case AdjustAboveDmgCap:
abstractEffectModifier = new AdjustAboveDmgCapEffectModifier(rs);
break;
case Ambidexterity:
abstractEffectModifier = new AmbidexterityEffectModifier(rs);
break;
case AnimOverride:
break;
case ArmorPiercing:
abstractEffectModifier = new ArmorPiercingEffectModifier(rs);
break;
case AttackDelay:
abstractEffectModifier = new AttackDelayEffectModifier(rs);
break;
case Attr:
abstractEffectModifier = new AttributeEffectModifier(rs);
break;
case BlackMantle:
abstractEffectModifier = new BlackMantleEffectModifier(rs);
break;
case BladeTrails:
abstractEffectModifier = new BladeTrailsEffectModifier(rs);
break;
case Block:
abstractEffectModifier = new BlockEffectModifier(rs);
break;
case BlockedPowerType:
abstractEffectModifier = new BlockedPowerTypeEffectModifier(rs);
break;
case CannotAttack:
abstractEffectModifier = new CannotAttackEffectModifier(rs);
break;
case CannotCast:
abstractEffectModifier = new CannotCastEffectModifier(rs);
break;
case CannotMove:
abstractEffectModifier = new CannotMoveEffectModifier(rs);
break;
case CannotTrack:
abstractEffectModifier = new CannotTrackEffectModifier(rs);
break;
case Charmed:
abstractEffectModifier = new CharmedEffectModifier(rs);
break;
case ConstrainedAmbidexterity:
abstractEffectModifier = new ConstrainedAmbidexterityEffectModifier(rs);
break;
case DamageCap:
abstractEffectModifier = new DamageCapEffectModifier(rs);
break;
case DamageShield:
abstractEffectModifier = new DamageShieldEffectModifier(rs);
break;
case DCV:
abstractEffectModifier = new DCVEffectModifier(rs);
break;
case Dodge:
abstractEffectModifier = new DodgeEffectModifier(rs);
break;
case DR:
abstractEffectModifier = new DREffectModifier(rs);
break;
case Durability:
abstractEffectModifier = new DurabilityEffectModifier(rs);
break;
case ExclusiveDamageCap:
abstractEffectModifier = new ExclusiveDamageCapEffectModifier(rs);
break;
case Fade:
abstractEffectModifier = new FadeEffectModifier(rs);
break;
case Fly:
abstractEffectModifier = new FlyEffectModifier(rs);
break;
case Health:
abstractEffectModifier = new HealthEffectModifier(rs);
break;
case HealthFull:
abstractEffectModifier = new HealthFullEffectModifier(rs);
break;
case HealthRecoverRate:
abstractEffectModifier = new HealthRecoverRateEffectModifier(rs);
break;
case IgnoreDamageCap:
abstractEffectModifier = new IgnoreDamageCapEffectModifier(rs);
break;
case IgnorePassiveDefense:
abstractEffectModifier = new IgnorePassiveDefenseEffectModifier(rs);
break;
case ImmuneTo:
abstractEffectModifier = new ImmuneToEffectModifier(rs);
break;
case ImmuneToAttack:
abstractEffectModifier = new ImmuneToAttackEffectModifier(rs);
break;
case ImmuneToPowers:
abstractEffectModifier = new ImmuneToPowersEffectModifier(rs);
break;
case Invisible:
abstractEffectModifier = new InvisibleEffectModifier(rs);
break;
case ItemName:
abstractEffectModifier = new ItemNameEffectModifier(rs);
if (((ItemNameEffectModifier) abstractEffectModifier).name.isEmpty())
break;
if (effectBase != null)
effectBase.setName((((ItemNameEffectModifier) abstractEffectModifier).name));
break;
case Mana:
abstractEffectModifier = new ManaEffectModifier(rs);
break;
case ManaFull:
abstractEffectModifier = new ManaFullEffectModifier(rs);
break;
case ManaRecoverRate:
abstractEffectModifier = new ManaRecoverRateEffectModifier(rs);
break;
case MaxDamage:
abstractEffectModifier = new MaxDamageEffectModifier(rs);
break;
case MeleeDamageModifier:
abstractEffectModifier = new MeleeDamageEffectModifier(rs);
break;
case MinDamage:
abstractEffectModifier = new MinDamageEffectModifier(rs);
break;
case NoMod:
abstractEffectModifier = new NoModEffectModifier(rs);
break;
case OCV:
abstractEffectModifier = new OCVEffectModifier(rs);
break;
case Parry:
abstractEffectModifier = new ParryEffectModifier(rs);
break;
case PassiveDefense:
abstractEffectModifier = new PassiveDefenseEffectModifier(rs);
break;
case PowerCost:
abstractEffectModifier = new PowerCostEffectModifier(rs);
break;
case PowerCostHealth:
abstractEffectModifier = new PowerCostHealthEffectModifier(rs);
break;
case PowerDamageModifier:
abstractEffectModifier = new PowerDamageEffectModifier(rs);
break;
case ProtectionFrom:
abstractEffectModifier = new ProtectionFromEffectModifier(rs);
break;
case Resistance:
abstractEffectModifier = new ResistanceEffectModifier(rs);
break;
case ScaleHeight:
abstractEffectModifier = new ScaleHeightEffectModifier(rs);
break;
case ScaleWidth:
abstractEffectModifier = new ScaleWidthEffectModifier(rs);
break;
case ScanRange:
abstractEffectModifier = new ScanRangeEffectModifier(rs);
break;
case SeeInvisible:
abstractEffectModifier = new SeeInvisibleEffectModifier(rs);
break;
case Silenced:
abstractEffectModifier = new SilencedEffectModifier(rs);
break;
case Skill:
abstractEffectModifier = new SkillEffectModifier(rs);
break;
case Slay:
abstractEffectModifier = new SlayEffectModifier(rs);
break;
case Speed:
abstractEffectModifier = new SpeedEffectModifier(rs);
break;
case SpireBlock:
abstractEffectModifier = new SpireBlockEffectModifier(rs);
break;
case Stamina:
abstractEffectModifier = new StaminaEffectModifier(rs);
break;
case StaminaFull:
abstractEffectModifier = new StaminaFullEffectModifier(rs);
break;
case StaminaRecoverRate:
abstractEffectModifier = new StaminaRecoverRateEffectModifier(rs);
break;
case Stunned:
abstractEffectModifier = new StunnedEffectModifier(rs);
break;
case Value:
abstractEffectModifier = new ValueEffectModifier(rs);
if (effectBase != null) {
ValueEffectModifier valueEffect = (ValueEffectModifier) abstractEffectModifier;
effectBase.setValue(valueEffect.minMod);
}
break;
case WeaponProc:
abstractEffectModifier = new WeaponProcEffectModifier(rs);
break;
case WeaponRange:
abstractEffectModifier = new WeaponRangeEffectModifier(rs);
break;
case WeaponSpeed:
abstractEffectModifier = new WeaponSpeedEffectModifier(rs);
break;
}
return abstractEffectModifier;
}
}
@@ -1,58 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.gameManager.PowersManager;
import engine.mbEnums;
import engine.objects.EffectsResourceCosts;
import org.json.JSONObject;
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 dbEffectsResourceCostHandler extends dbHandlerBase {
public dbEffectsResourceCostHandler() {
this.localClass = EffectsResourceCosts.class;
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public void LOAD_ALL_COSTMAPS() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_effect_costmaps`")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
String effectID = rs.getString("effectID");
String costString = rs.getString("costmap");
JSONObject costJSON = new JSONObject(costString);
HashMap<mbEnums.ResourceType, Integer> costmap = new HashMap<>();
for (String key : costJSON.keySet()) {
int value = costJSON.getInt(key);
costmap.put(mbEnums.ResourceType.valueOf(key), value);
}
PowersManager._effect_costMaps.put(effectID, costmap);
}
} catch (SQLException e) {
Logger.error(e);
}
}
}
+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;
}
}
+9 -35
View File
@@ -16,8 +16,6 @@ import engine.mbEnums.ItemType;
import engine.objects.AbstractCharacter;
import engine.objects.CharacterItemManager;
import engine.objects.Item;
import engine.objects.ItemTemplate;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
@@ -177,47 +175,23 @@ public class dbItemHandler extends dbHandlerBase {
return itemList;
}
public void LOAD_ITEM_TEMPLATES() {
public HashMap<Integer, Integer> LOAD_TEMPLATE_MODTABLES() {
HashMap<ItemType, Integer> templateTCountMap = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_item_templates`;");
ResultSet rs = preparedStatement.executeQuery()) {
while (rs.next()) {
int templateID = rs.getInt("id");
JSONObject jsonObject = new JSONObject(rs.getString("template"));
ItemTemplate itemTemplate = new ItemTemplate(jsonObject);
itemTemplate.template_id = templateID;
ItemTemplate.templates.put(templateID, itemTemplate);
templateTCountMap.merge(itemTemplate.item_type, 1, Integer::sum);
}
Logger.info(templateTCountMap.toString());
} catch (Exception e) {
Logger.error(e);
}
}
public void LOAD_TEMPLATE_MODTABLES() {
HashMap<Integer, Integer> modTables = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_vendor_items`;");
ResultSet rs = preparedStatement.executeQuery()) {
while (rs.next()) {
int templateID = rs.getInt("templateID");
int modTable = rs.getInt("modTable");
ItemTemplate template = ItemTemplate.templates.get(templateID);
template.modTable = modTable;
}
while (rs.next())
modTables.put(rs.getInt("templateID"), rs.getInt("modTable"));
} catch (Exception e) {
Logger.error(e);
}
Logger.info("read: " + modTables.size());
return modTables;
}
public Item GET_ITEM(final int itemUUID) {
@@ -325,7 +299,7 @@ public class dbItemHandler extends dbHandlerBase {
for (Item item : inventory) {
if (item.template.item_type.equals(ItemType.GOLD))
if (item.template.getItemType().equals(ItemType.GOLD))
continue;
try (Connection connection = DbManager.getConnection();
@@ -474,7 +448,7 @@ public class dbItemHandler extends dbHandlerBase {
*/
public boolean UPDATE_GOLD(final Item item, int newValue, int oldValue) {
if (!item.template.item_type.equals(ItemType.GOLD))
if (!item.template.getItemType().equals(ItemType.GOLD))
return false;
try (Connection connection = DbManager.getConnection();
@@ -554,7 +528,7 @@ public class dbItemHandler extends dbHandlerBase {
// Write 0 if we will not modify the value from template
if (value == item.template.item_value)
if (value == item.template.getItemValue())
value = 0;
try (Connection connection = DbManager.getConnection();
-46
View File
@@ -1,46 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.objects.Kit;
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 class dbKitHandler extends dbHandlerBase {
public dbKitHandler() {
this.localClass = Kit.class;
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public ArrayList<Kit> GET_ALL_KITS() {
ArrayList<Kit> kitList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_validkit`")) {
ResultSet rs = preparedStatement.executeQuery();
kitList = getObjectsFromRs(rs, 20);
} catch (SQLException e) {
Logger.error(e);
}
return kitList;
}
}
-17
View File
@@ -11,7 +11,6 @@ package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.loot.*;
import engine.objects.Item;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
@@ -221,20 +220,4 @@ public class dbLootHandler extends dbHandlerBase {
return bootySets;
}
public void LOAD_ENCHANT_VALUES() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `IDString`, `minMod` FROM `static_power_effectmod` WHERE `modType` = ?")) {
preparedStatement.setString(1, "Value");
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
Item.addEnchantValue(rs.getString("IDString"), rs.getInt("minMod"));
} catch (SQLException e) {
Logger.error(e);
}
}
}
+3 -66
View File
@@ -13,8 +13,6 @@ import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.mbEnums.GameObjectType;
import engine.objects.MobBase;
import engine.objects.MobBaseEffects;
import engine.objects.MobBaseStats;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
@@ -69,71 +67,10 @@ public class dbMobBaseHandler extends dbHandlerBase {
} catch (SQLException e) {
Logger.error(e);
}
Logger.info("Loaded " + mobbaseList.size() + " mobbases");
return mobbaseList;
}
public ArrayList<MobBaseEffects> GET_RUNEBASE_EFFECTS(int runeID) {
ArrayList<MobBaseEffects> effectsList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_npc_mobbase_effects` WHERE `mobbaseUUID` = ?")) {
preparedStatement.setInt(1, runeID);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
MobBaseEffects mbs = new MobBaseEffects(rs);
effectsList.add(mbs);
}
} catch (SQLException e) {
Logger.error(e);
}
return effectsList;
}
public MobBaseStats LOAD_STATS(int mobBaseUUID) {
MobBaseStats mobBaseStats = MobBaseStats.GetGenericStats();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_npc_mobbase_stats` WHERE `mobbaseUUID` = ?")) {
preparedStatement.setInt(1, mobBaseUUID);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
mobBaseStats = new MobBaseStats(rs);
} catch (SQLException e) {
Logger.error(e);
}
return mobBaseStats;
}
public void LOAD_ALL_MOBBASE_SPEEDS(MobBase mobBase) {
if (mobBase.getLoadID() == 0)
return;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_npc_mobbase_race` WHERE `mobbaseID` = ?")) {
preparedStatement.setInt(1, mobBase.getLoadID());
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
float walk = rs.getFloat("walkStandard");
float walkCombat = rs.getFloat("walkCombat");
float run = rs.getFloat("runStandard");
float runCombat = rs.getFloat("runCombat");
mobBase.updateSpeeds(walk, walkCombat, run, runCombat);
}
} catch (SQLException e) {
Logger.error(e);
}
}
}
+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(?,?,?)")) {
+37 -9
View File
@@ -57,7 +57,7 @@ public class dbNPCHandler extends dbHandlerBase {
runeSets.put(runeSetID, runeList);
} else {
ArrayList<Integer> runeList = runeSets.get(runeSetID);
runeList.add(runeSetID);
runeList.add(runeBaseID);
runeSets.put(runeSetID, runeList);
}
}
@@ -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 (?)")) {
@@ -1,82 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.gameManager.PowersManager;
import engine.mbEnums;
import engine.objects.Mob;
import engine.powers.EffectsBase;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashSet;
public class dbPowerHandler extends dbHandlerBase {
public dbPowerHandler() {
this.localClass = Mob.class;
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public static void addAllSourceTypes() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_power_sourcetype")) {
ResultSet rs = preparedStatement.executeQuery();
String IDString, source;
while (rs.next()) {
IDString = rs.getString("IDString");
int token = DbManager.hasher.SBStringHash(IDString);
source = rs.getString("source").replace("-", "").trim();
mbEnums.EffectSourceType effectSourceType = mbEnums.EffectSourceType.GetEffectSourceType(source);
if (EffectsBase.effectSourceTypeMap.containsKey(token) == false)
EffectsBase.effectSourceTypeMap.put(token, new HashSet<>());
EffectsBase.effectSourceTypeMap.get(token).add(effectSourceType);
}
} catch (Exception e) {
Logger.error(e);
}
}
public static void addAllAnimationOverrides() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_power_animation_override")) {
ResultSet rs = preparedStatement.executeQuery();
String IDString;
int animation;
while (rs.next()) {
IDString = rs.getString("IDString");
EffectsBase eb = PowersManager.getEffectByIDString(IDString);
if (eb != null)
IDString = eb.getIDString();
animation = rs.getInt("animation");
PowersManager.AnimationOverrides.put(IDString, animation);
}
} catch (Exception e) {
Logger.error(e);
}
}
}
@@ -27,30 +27,17 @@ public class dbPromotionClassHandler extends dbHandlerBase {
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public ArrayList<Integer> GET_ALLOWED_RUNES(final PromotionClass pc) {
ArrayList<Integer> runeList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_promotionrunereq` WHERE `promoID`=?")) {
preparedStatement.setInt(1, pc.getObjectUUID());
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
runeList.add(rs.getInt("runereqID"));
} catch (SQLException e) {
Logger.error(e);
}
return runeList;
}
public PromotionClass GET_PROMOTION_CLASS(final int objectUUID) {
PromotionClass promotionClass = null;
PromotionClass promotionClass;
if (objectUUID == 0)
return null;
promotionClass = (PromotionClass) DbManager.getFromCache(mbEnums.GameObjectType.PromotionClass, objectUUID);
if (promotionClass != null)
return promotionClass;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_promotion` WHERE `ID` = ?")) {
+7 -60
View File
@@ -10,15 +10,14 @@
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.objects.Race;
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;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
public class dbRaceHandler extends dbHandlerBase {
@@ -46,27 +45,6 @@ public class dbRaceHandler extends dbHandlerBase {
return integerSet;
}
public HashSet<Integer> BEARD_STYLES_FOR_RACE(final int id) {
HashSet<Integer> integerSet = new HashSet<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `beardStyle` FROM `static_rune_racebeardstyle` WHERE `RaceID` = ?")) {
preparedStatement.setInt(1, id);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
integerSet.add(rs.getInt(1));
} catch (SQLException e) {
Logger.error(e);
}
return integerSet;
}
public HashSet<Integer> HAIR_COLORS_FOR_RACE(final int id) {
HashSet<Integer> integerSet = new HashSet<>();
@@ -89,28 +67,6 @@ public class dbRaceHandler extends dbHandlerBase {
}
public HashSet<Integer> HAIR_STYLES_FOR_RACE(final int id) {
HashSet<Integer> integerSet = new HashSet<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `hairStyle` FROM `static_rune_racehairstyle` WHERE `RaceID` = ?")) {
preparedStatement.setInt(1, id);
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
integerSet.add(rs.getInt(1));
} catch (SQLException e) {
Logger.error(e);
}
return integerSet;
}
public HashSet<Integer> SKIN_COLOR_FOR_RACE(final int id) {
HashSet<Integer> integerSet = new HashSet<>();
@@ -132,32 +88,23 @@ public class dbRaceHandler extends dbHandlerBase {
return integerSet;
}
public ConcurrentHashMap<Integer, Race> LOAD_ALL_RACES() {
// Rune id -> {minHeight, strHeightMod}; the rows also decide which race runes are player races
public HashMap<Integer, float[]> LOAD_RACE_HEIGHTS() {
ConcurrentHashMap<Integer, Race> races;
Race thisRace;
races = new ConcurrentHashMap<>();
int recordsRead = 0;
HashMap<Integer, float[]> raceHeights = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_rune_race")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
recordsRead++;
thisRace = new Race(rs);
races.put(thisRace.getRaceRuneID(), thisRace);
}
while (rs.next())
raceHeights.put(rs.getInt("ID"), new float[]{rs.getFloat("minHeight"), rs.getFloat("strHeightMod")});
} catch (SQLException e) {
Logger.error(e);
}
Logger.info("read: " + recordsRead + " cached: " + races.size());
return races;
return raceHeights;
}
}
@@ -1,47 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.objects.Resists;
import org.pmw.tinylog.Logger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class dbResistHandler extends dbHandlerBase {
public dbResistHandler() {
}
public Resists GET_RESISTS_FOR_MOB(int resistID) {
Resists resists = null;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_npc_mob_resists` WHERE `ID` = ?;")) {
preparedStatement.setInt(1, resistID);
ResultSet rs = preparedStatement.executeQuery();
if (rs.next())
resists = new Resists(rs);
} catch (SQLException e) {
Logger.error(e);
}
return resists;
}
}
@@ -1,48 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.objects.RuneBaseAttribute;
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 class dbRuneBaseAttributeHandler extends dbHandlerBase {
public dbRuneBaseAttributeHandler() {
this.localClass = RuneBaseAttribute.class;
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public ArrayList<RuneBaseAttribute> GET_ATTRIBUTES_FOR_RUNEBASE() {
ArrayList<RuneBaseAttribute> runeBaseAttributesList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_runebaseattribute`")) {
ResultSet rs = preparedStatement.executeQuery();
runeBaseAttributesList = getObjectsFromRs(rs, 10);
} catch (SQLException e) {
Logger.error(e);
}
return runeBaseAttributesList;
}
}
@@ -1,92 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.mbEnums.GameObjectType;
import engine.objects.AbstractGameObject;
import engine.objects.RuneBaseEffect;
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;
import java.util.HashMap;
public class dbRuneBaseEffectHandler extends dbHandlerBase {
public dbRuneBaseEffectHandler() {
this.localClass = RuneBaseEffect.class;
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public ArrayList<RuneBaseEffect> GET_EFFECTS_FOR_RUNEBASE(int id) {
ArrayList<RuneBaseEffect> runeBaseEffectsList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_baseeffect` WHERE `runeID`=?")) {
preparedStatement.setInt(1, id);
ResultSet rs = preparedStatement.executeQuery();
runeBaseEffectsList = getObjectsFromRs(rs, 250);
} catch (SQLException e) {
Logger.error(e);
}
return runeBaseEffectsList;
}
public ArrayList<RuneBaseEffect> GET_ALL_RUNEBASE_EFFECTS() {
ArrayList<RuneBaseEffect> runeBaseEffectsList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_baseeffect`;")) {
ResultSet rs = preparedStatement.executeQuery();
runeBaseEffectsList = getObjectsFromRs(rs, 250);
} catch (SQLException e) {
Logger.error(e);
}
return runeBaseEffectsList;
}
//This calls from cache only. Call this AFTER caching all runebase effects;
public HashMap<Integer, ArrayList<RuneBaseEffect>> LOAD_BASEEFFECTS_FOR_RUNEBASE() {
HashMap<Integer, ArrayList<RuneBaseEffect>> runeBaseEffectSet;
runeBaseEffectSet = new HashMap<>();
for (AbstractGameObject runeBaseEffect : DbManager.getList(GameObjectType.RuneBaseEffect)) {
int runeBaseID = ((RuneBaseEffect) runeBaseEffect).getRuneBaseID();
if (runeBaseEffectSet.get(runeBaseID) == null) {
ArrayList<RuneBaseEffect> runeBaseEffectList = new ArrayList<>();
runeBaseEffectList.add((RuneBaseEffect) runeBaseEffect);
runeBaseEffectSet.put(runeBaseID, runeBaseEffectList);
} else {
ArrayList<RuneBaseEffect> runeBaseEffectList = runeBaseEffectSet.get(runeBaseID);
runeBaseEffectList.add((RuneBaseEffect) runeBaseEffect);
runeBaseEffectSet.put(runeBaseID, runeBaseEffectList);
}
}
return runeBaseEffectSet;
}
}
+39 -214
View File
@@ -11,17 +11,16 @@ package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.objects.AbstractGameObject;
import engine.objects.BaseClass;
import engine.objects.PromotionClass;
import engine.objects.Race;
import engine.objects.RuneBase;
import engine.powers.RunePowerEntry;
import engine.powers.RuneSkillAdjustEntry;
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;
import java.util.HashMap;
public class dbRuneBaseHandler extends dbHandlerBase {
@@ -30,231 +29,57 @@ public class dbRuneBaseHandler extends dbHandlerBase {
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public static HashMap<Integer, ArrayList<RunePowerEntry>> LOAD_RUNE_POWERS() {
// static_rune_lookup is generated for the DB views; a failed write must never stop the boot
public static void WRITE_RUNE_LOOKUP() {
HashMap<Integer, ArrayList<RunePowerEntry>> mobPowers = new HashMap<>();
RunePowerEntry runePowerEntry;
int rune_id;
int recordsRead = 0;
int rowsWritten = 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_rune_powers")) {
PreparedStatement deleteStatement = connection.prepareStatement("DELETE FROM `static_rune_lookup`");
PreparedStatement insertStatement = connection.prepareStatement("INSERT INTO `static_rune_lookup` (`ID`, `type`, `name`) VALUES (?, ?, ?)")) {
ResultSet rs = preparedStatement.executeQuery();
connection.setAutoCommit(false);
while (rs.next()) {
try {
deleteStatement.executeUpdate();
recordsRead++;
rune_id = rs.getInt("rune_id");
runePowerEntry = new RunePowerEntry(rs);
if (mobPowers.get(rune_id) == null) {
ArrayList<RunePowerEntry> runePowerList = new ArrayList<>();
runePowerList.add(runePowerEntry);
mobPowers.put(rune_id, runePowerList);
} else {
ArrayList<RunePowerEntry> powerList = mobPowers.get(rune_id);
powerList.add(runePowerEntry);
mobPowers.put(rune_id, powerList);
for (Race race : Race.getAllRaces()) {
addRuneLookupRow(insertStatement, race.getRaceRuneID(), "RACE", race.getName());
rowsWritten++;
}
}
} catch (SQLException e) {
Logger.error(e);
return mobPowers;
}
Logger.info("read: " + recordsRead + " cached: " + mobPowers.size());
return mobPowers;
}
public static HashMap<Integer, ArrayList<RuneSkillAdjustEntry>> LOAD_RUNE_SKILL_ADJUSTS() {
HashMap<Integer, ArrayList<RuneSkillAdjustEntry>> runeSkillAdjusts = new HashMap<>();
RuneSkillAdjustEntry runeSkillAdjustEntry;
int rune_id;
int recordsRead = 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_rune_skill_adjusts")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
recordsRead++;
rune_id = rs.getInt("rune_id");
runeSkillAdjustEntry = new RuneSkillAdjustEntry(rs);
if (runeSkillAdjusts.get(rune_id) == null) {
ArrayList<RuneSkillAdjustEntry> skillAdjustList = new ArrayList<>();
skillAdjustList.add(runeSkillAdjustEntry);
runeSkillAdjusts.put(rune_id, skillAdjustList);
} else {
ArrayList<RuneSkillAdjustEntry> powerList = runeSkillAdjusts.get(rune_id);
powerList.add(runeSkillAdjustEntry);
runeSkillAdjusts.put(rune_id, powerList);
for (AbstractGameObject baseClass : DbManager.getMap(mbEnums.GameObjectType.BaseClass).values()) {
addRuneLookupRow(insertStatement, baseClass.getObjectUUID(), "BASECLASS", ((BaseClass) baseClass).getName());
rowsWritten++;
}
}
} catch (SQLException e) {
Logger.error(e);
return runeSkillAdjusts;
}
Logger.info("read: " + recordsRead + " cached: " + runeSkillAdjusts.size());
return runeSkillAdjusts;
}
public void GET_RUNE_REQS(final RuneBase rb) {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_runereq` WHERE `runeID` = ?")) {
preparedStatement.setInt(1, rb.getObjectUUID());
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
int type = rs.getInt("type");
switch (type) {
case 1:
rb.getRace().put(rs.getInt("requiredRuneID"), rs.getBoolean("isAllowed"));
break;
case 2:
rb.getBaseClass().put(rs.getInt("requiredRuneID"), rs.getBoolean("isAllowed"));
break;
case 3:
rb.getPromotionClass().put(rs.getInt("requiredRuneID"), rs.getBoolean("isAllowed"));
break;
case 4:
rb.getDiscipline().put(rs.getInt("requiredRuneID"), rs.getBoolean("isAllowed"));
break;
case 5:
rb.getOverwrite().add(rs.getInt("requiredRuneID"));
break;
case 6:
rb.setLevelRequired(rs.getInt("requiredRuneID"));
break;
for (AbstractGameObject promotionClass : DbManager.getMap(mbEnums.GameObjectType.PromotionClass).values()) {
addRuneLookupRow(insertStatement, promotionClass.getObjectUUID(), "PROMOTION", ((PromotionClass) promotionClass).getName());
rowsWritten++;
}
insertStatement.executeBatch();
connection.commit();
} catch (SQLException | RuntimeException e) {
// Roll back before setAutoCommit(true), which would otherwise commit the delete
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
} catch (SQLException e) {
Logger.error(e);
} catch (Exception e) {
Logger.warn("static_rune_lookup not written; DB views keep their old names: " + e.getMessage());
return;
}
Logger.info("static_rune_lookup: wrote " + rowsWritten + " rows");
}
public RuneBase GET_RUNEBASE(final int id) {
RuneBase runeBase = null;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_runebase` WHERE `ID` = ?")) {
preparedStatement.setInt(1, id);
ResultSet rs = preparedStatement.executeQuery();
runeBase = (RuneBase) getObjectFromRs(rs);
} catch (SQLException e) {
Logger.error(e);
}
return runeBase;
}
public ArrayList<RuneBase> LOAD_ALL_RUNEBASES() {
ArrayList<RuneBase> runeBasesList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_runebase`;")) {
ResultSet rs = preparedStatement.executeQuery();
runeBasesList = getObjectsFromRs(rs, 1000);
} catch (SQLException e) {
Logger.error(e);
}
return runeBasesList;
}
public HashMap<Integer, ArrayList<Integer>> LOAD_ALLOWED_STARTING_RUNES_FOR_BASECLASS() {
HashMap<Integer, ArrayList<Integer>> runeSets = new HashMap<>();
int recordsRead = 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_rune_baseclassrune")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
recordsRead++;
int baseClassID = rs.getInt("BaseClassesID");
int runeBaseID = rs.getInt("RuneBaseID");
if (runeSets.get(baseClassID) == null) {
ArrayList<Integer> runeList = new ArrayList<>();
runeList.add(runeBaseID);
runeSets.put(baseClassID, runeList);
} else {
ArrayList<Integer> runeList = runeSets.get(baseClassID);
runeList.add(runeBaseID);
runeSets.put(baseClassID, runeList);
}
}
} catch (SQLException e) {
Logger.error(e);
}
Logger.info("read: " + recordsRead + " cached: " + runeSets.size());
return runeSets;
}
public HashMap<Integer, ArrayList<Integer>> LOAD_ALLOWED_STARTING_RUNES_FOR_RACE() {
HashMap<Integer, ArrayList<Integer>> runeSets;
runeSets = new HashMap<>();
int recordsRead = 0;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_rune_racerune")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
recordsRead++;
int raceID = rs.getInt("RaceID");
int runeBaseID = rs.getInt("RuneBaseID");
if (runeSets.get(raceID) == null) {
ArrayList<Integer> runeList = new ArrayList<>();
runeList.add(runeBaseID);
runeSets.put(raceID, runeList);
} else {
ArrayList<Integer> runeList = runeSets.get(raceID);
runeList.add(runeBaseID);
runeSets.put(raceID, runeList);
}
}
} catch (SQLException e) {
Logger.error(e);
}
Logger.info("read: " + recordsRead + " cached: " + runeSets.size());
return runeSets;
private static void addRuneLookupRow(PreparedStatement insertStatement, int runeID, String type, String name) throws SQLException {
insertStatement.setInt(1, runeID);
insertStatement.setString(2, type);
insertStatement.setString(3, name);
insertStatement.addBatch();
}
}
+10 -101
View File
@@ -12,7 +12,6 @@ package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.mbEnums.GameObjectType;
import engine.objects.MaxSkills;
import engine.objects.SkillsBase;
import org.pmw.tinylog.Logger;
@@ -20,7 +19,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
public class dbSkillBaseHandler extends dbHandlerBase {
@@ -31,122 +29,33 @@ public class dbSkillBaseHandler extends dbHandlerBase {
}
public SkillsBase GET_BASE(final int objectUUID) {
SkillsBase skillsBase = (SkillsBase) DbManager.getFromCache(GameObjectType.SkillsBase, objectUUID);
if (skillsBase != null)
return skillsBase;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_skill_skillsbase WHERE ID = ?")) {
preparedStatement.setInt(1, objectUUID);
ResultSet rs = preparedStatement.executeQuery();
skillsBase = (SkillsBase) getObjectFromRs(rs);
} catch (SQLException e) {
Logger.error(e);
}
SkillsBase.putInCache(skillsBase);
return skillsBase;
return (SkillsBase) DbManager.getFromCache(GameObjectType.SkillsBase, objectUUID);
}
public SkillsBase GET_BASE_BY_NAME(String name) {
SkillsBase skillsBase = SkillsBase.getFromCache(name);
if (skillsBase != null)
return skillsBase;
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_skill_skillsbase WHERE name = ?")) {
preparedStatement.setString(1, name);
ResultSet rs = preparedStatement.executeQuery();
skillsBase = (SkillsBase) getObjectFromRs(rs);
} catch (SQLException e) {
Logger.error(e);
}
SkillsBase.putInCache(skillsBase);
return skillsBase;
return SkillsBase.getFromCache(name);
}
public SkillsBase GET_BASE_BY_TOKEN(final int token) {
return SkillsBase.getFromCache(token);
}
SkillsBase skillsBase = SkillsBase.getFromCache(token);
public HashMap<Integer, String> GET_ALL_SKILL_NAMES() {
if (skillsBase != null)
return skillsBase;
HashMap<Integer, String> skillNames = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_skill_skillsbase WHERE token = ?")) {
PreparedStatement preparedStatement = connection.prepareStatement("SELECT ID, name FROM static_skill_skillsbase")) {
preparedStatement.setInt(1, token);
ResultSet rs = preparedStatement.executeQuery();
skillsBase = (SkillsBase) getObjectFromRs(rs);
while (rs.next())
skillNames.put(rs.getInt("ID"), rs.getString("name"));
} catch (SQLException e) {
Logger.error(e);
}
SkillsBase.putInCache(skillsBase);
return skillsBase;
return skillNames;
}
public void LOAD_ALL_MAX_SKILLS_FOR_CONTRACT() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_rune_maxskills`")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
MaxSkills maxSKills = new MaxSkills(rs);
if (MaxSkills.MaxSkillsSet.get(maxSKills.getRuneID()) == null) {
ArrayList<MaxSkills> newMaxSkillsList = new ArrayList<>();
newMaxSkillsList.add(maxSKills);
MaxSkills.MaxSkillsSet.put(maxSKills.getRuneID(), newMaxSkillsList);
} else
MaxSkills.MaxSkillsSet.get(maxSKills.getRuneID()).add(maxSKills);
}
} catch (SQLException e) {
Logger.error(e);
}
}
public void LOAD_ALL_RUNE_SKILLS() {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_skill_skillsgranted`")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
int runeID = rs.getInt("runeID");
int token = rs.getInt("token");
int amount = rs.getInt("amount");
if (SkillsBase.runeSkillsCache.get(runeID) == null)
SkillsBase.runeSkillsCache.put(runeID, new HashMap<>());
SkillsBase.runeSkillsCache.get(runeID).put(token, amount);
}
} catch (SQLException e) {
Logger.error(e);
}
}
}
@@ -9,18 +9,8 @@
package engine.db.handlers;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.objects.SkillReq;
import engine.powers.PowersBase;
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;
import java.util.HashMap;
public class dbSkillReqHandler extends dbHandlerBase {
@@ -28,72 +18,4 @@ public class dbSkillReqHandler extends dbHandlerBase {
this.localClass = SkillReq.class;
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
}
public static ArrayList<PowersBase> getAllPowersBase() {
ArrayList<PowersBase> powerBaseList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM static_power_powerbase")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
PowersBase toAdd = new PowersBase(rs);
powerBaseList.add(toAdd);
}
} catch (SQLException e) {
Logger.error(e);
}
return powerBaseList;
}
public static void getFailConditions(HashMap<String, PowersBase> powers) {
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT IDString, type FROM static_power_failcondition where powerOrEffect = 'Power'")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
String type = rs.getString("type");
String IDString = rs.getString("IDString");
PowersBase pb = powers.get(IDString);
if (pb != null)
switch (type) {
case "CastSpell":
pb.cancelOnCastSpell = true;
break;
case "TakeDamage":
pb.cancelOnTakeDamage = true;
break;
}
}
} catch (SQLException e) {
Logger.error(e);
}
}
public ArrayList<SkillReq> GET_REQS_FOR_RUNE(final int objectUUID) {
ArrayList<SkillReq> skillReqsList = new ArrayList<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `static_skill_skillreq` WHERE `runeID`=?")) {
preparedStatement.setInt(1, objectUUID);
ResultSet rs = preparedStatement.executeQuery();
skillReqsList = getObjectsFromRs(rs, 5);
} catch (SQLException e) {
Logger.error(e);
}
return skillReqsList;
}
}
@@ -0,0 +1,87 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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;
}
}
+19 -5
View File
@@ -22,6 +22,7 @@ import engine.objects.Warehouse;
import engine.server.MBServerStatics;
import org.joda.time.DateTime;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.pmw.tinylog.Logger;
@@ -134,7 +135,6 @@ public class dbWarehouseHandler extends dbHandlerBase {
while (rs.next()) {
int cityUID = rs.getInt("cityUUID");
JSONObject jsonObject = new JSONObject(rs.getString("warehouse"));
City city = City.getCity(cityUID);
if (city == null) {
@@ -142,12 +142,18 @@ public class dbWarehouseHandler extends dbHandlerBase {
continue;
}
city.warehouse = new Warehouse(jsonObject);
try {
city.warehouse = new Warehouse(new JSONObject(rs.getString("warehouse")));
} catch (JSONException e) {
Logger.error("Warehouse for city " + cityUID + " failed to load: " + e.getMessage());
continue;
}
city.warehouse.city = city;
// Locate warehouse building
for (Building building : city.parentZone.zoneBuildingSet) {
if (building.getBlueprint().getBuildingGroup().equals(mbEnums.BuildingGroup.WAREHOUSE)) {
if (building.getBlueprint().getBuildingGroup().equals(mbEnums.BuildingGroup.WAREHOUSE) && building.getRank() != -1) {
city.warehouse.building = building;
break;
}
@@ -205,8 +211,16 @@ public class dbWarehouseHandler extends dbHandlerBase {
ResultSet rs = preparedStatement.executeQuery()) {
while (rs.next()) {
JSONObject jsonObject = new JSONObject(rs.getString("workorder"));
WorkOrder workOrder = new WorkOrder(jsonObject);
int workOrderID = rs.getInt("workorderID");
WorkOrder workOrder;
try {
workOrder = new WorkOrder(new JSONObject(rs.getString("workorder")));
} catch (JSONException e) {
Logger.error("WorkOrder " + workOrderID + " failed to load: " + e.getMessage());
continue;
}
submitList.add(workOrder);
}
} catch (Exception e) {
+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'")) {
if (DbManager.refuseStaticWrite(zone))
return false;
preparedStatement.setInt(1, zone.getObjectUUID());
return (preparedStatement.executeUpdate() > 0);
} catch (SQLException e) {
Logger.error(e);
}
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());
}
}
+12 -13
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,
localLoc, 1.0f, 0, ProtectionState.PROTECTED, 0, rank,
null, blueprintUUID, w, rot.y);
Building building = Building.persistBuilding(Building.createBuilding(
zone, 0, buildingName, ID,
localLoc, 1.0f, 0, ProtectionState.PROTECTED, 0, rank,
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
@@ -15,8 +15,8 @@ import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.objects.AbstractGameObject;
import engine.objects.Item;
import engine.objects.ItemTemplate;
import engine.objects.PlayerCharacter;
import engine.parsers.objects.CObjectTemplate;
import org.pmw.tinylog.Logger;
/**
@@ -38,7 +38,7 @@ public class CreateItemCmd extends AbstractDevCmd {
}
int templateID = Integer.parseInt(words[0]);
ItemTemplate template = ItemTemplate.templates.get(templateID);
CObjectTemplate template = CObjectTemplate.lookup(templateID);
if (template == null) {
ChatManager.chatSystemInfo(playerCharacter, "No such template found.");
@@ -51,7 +51,7 @@ public class CreateItemCmd extends AbstractDevCmd {
for (int i = 0; i < size; i++) {
if (!playerCharacter.charItemManager.hasRoomInventory(template.item_wt)) {
if (!playerCharacter.charItemManager.hasRoomInventory(template.getItemWt())) {
ChatManager.chatSystemInfo(playerCharacter, "You are encumbered!.");
break;
}
+106
View File
@@ -0,0 +1,106 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.devcmd.cmds;
import engine.devcmd.AbstractDevCmd;
import engine.gameManager.ChatManager;
import engine.gameManager.DbManager;
import engine.mbEnums;
import engine.objects.AbstractGameObject;
import engine.objects.Building;
import engine.objects.City;
import engine.objects.PlayerCharacter;
import engine.objects.Warehouse;
import java.util.HashMap;
public class FillCmd extends AbstractDevCmd {
public FillCmd() {
super("fill");
}
@Override
protected void _doCmd(PlayerCharacter pc, String[] words,
AbstractGameObject target) {
Building building;
City city;
Warehouse warehouse;
int amount = Integer.MAX_VALUE;
HashMap<mbEnums.ResourceType, Integer> oldResources;
if (words.length > 1) {
this.sendUsage(pc);
return;
}
if (words.length == 1 && !words[0].isEmpty()) {
try {
amount = Integer.parseInt(words[0]);
} catch (NumberFormatException e) {
throwbackError(pc, "Amount must be a number, " + words[0] + " is invalid");
return;
}
if (amount < 0) {
throwbackError(pc, "Amount must be 0 or more");
return;
}
}
if (target == null || target.getObjectType() != mbEnums.GameObjectType.Building) {
throwbackError(pc, "Select a warehouse");
return;
}
building = (Building) target;
if (building.getBlueprint() == null || building.getBlueprint().getBuildingGroup() != mbEnums.BuildingGroup.WAREHOUSE) {
throwbackError(pc, "Selected building is not a warehouse");
return;
}
city = building.getCity();
if (city == null || city.warehouse == null || city.warehouse.building == null
|| city.warehouse.building.getObjectUUID() != building.getObjectUUID()) {
throwbackError(pc, "Selected warehouse is not the active warehouse of a city");
return;
}
warehouse = city.warehouse;
oldResources = new HashMap<>(warehouse.resources);
for (mbEnums.ResourceType resourceType : mbEnums.ResourceType.values())
warehouse.resources.put(resourceType, Math.min(amount, resourceType.deposit_limit));
if (!DbManager.WarehouseQueries.UPDATE_WAREHOUSE(warehouse)) {
warehouse.resources.putAll(oldResources);
throwbackError(pc, "Failed to update the warehouse in the database");
return;
}
ChatManager.chatSayInfo(pc, "Filled the " + city.getCityName() + " warehouse"
+ (amount == Integer.MAX_VALUE ? " to each deposit limit" : " with " + amount + " of each resource (capped at the deposit limit)"));
}
@Override
protected String _getHelpString() {
return "Sets every resource in the selected warehouse";
}
@Override
protected String _getUsageString() {
return "' ./fill [amount]' with a warehouse selected (no amount fills each resource to its deposit limit)";
}
}
+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));
+19 -9
View File
@@ -18,6 +18,8 @@ import engine.mbEnums.BuildingGroup;
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;
@@ -87,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;
@@ -105,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;
@@ -342,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);
@@ -413,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);
@@ -507,17 +509,17 @@ public class InfoCmd extends AbstractDevCmd {
case Item: //intentional passthrough
case MobLoot:
Item item = (Item) target;
ItemTemplate template = ItemTemplate.templates.get(item.templateID);
output += StringUtils.addWS("Template: " + template.template_id, 20);
output += "Weight: " + template.item_wt;
CObjectTemplate template = CObjectTemplate.lookup(item.templateID);
output += StringUtils.addWS("Template: " + template.getTemplateId(), 20);
output += "Weight: " + template.getItemWt();
output += newline;
DecimalFormat df = new DecimalFormat("###,###,###,###,##0");
output += StringUtils.addWS("Qty: "
+ df.format(item.getNumOfItems()), 20);
output += "Charges: " + (byte) item.chargesRemaining
+ '/' + item.template.item_initial_charges;
+ '/' + item.template.getItemInitialCharges();
output += newline;
output += "Name: " + template.item_base_name;
output += "Name: " + template.getItemBaseName();
output += newline;
output += item.getContainerInfo();
@@ -538,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.";
+16 -9
View File
@@ -11,9 +11,11 @@ package engine.devcmd.cmds;
import engine.devcmd.AbstractDevCmd;
import engine.gameManager.DbManager;
import engine.mbEnums.ItemContainerType;
import engine.mbEnums.ItemType;
import engine.mbEnums.OwnerType;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.EffectsBase;
/**
@@ -59,18 +61,17 @@ public class MakeItemCmd extends AbstractDevCmd {
}
int templateID;
ItemTemplate template;
CObjectTemplate template;
try {
templateID = Integer.parseInt(words[0]);
template = ItemTemplate.templates.get(words[0].toLowerCase());
template = CObjectTemplate.lookup(templateID);
if (template == null) {
throwbackError(pc, "Supplied type " + words[0]
+ " failed to parse to an Integer");
throwbackError(pc, "No item template with id " + words[0]);
return;
}
if (template.item_type == ItemType.GOLD) {
if (template.getItemType() == ItemType.GOLD) {
this.throwbackInfo(pc, "use /addgold to add gold.");
return;
}
@@ -132,7 +133,7 @@ public class MakeItemCmd extends AbstractDevCmd {
return;
}
}
template = ItemTemplate.templates.get(templateID);
template = CObjectTemplate.lookup(templateID);
if (template == null) {
throwbackError(pc, "Unable to find template " + templateID);
@@ -140,8 +141,8 @@ public class MakeItemCmd extends AbstractDevCmd {
}
if ((numItems > 1)
&& (template.item_type.equals(ItemType.RESOURCE) == false)
&& (template.item_type.equals(ItemType.OFFERING)) == false)
&& (template.getItemType().equals(ItemType.RESOURCE) == false)
&& (template.getItemType().equals(ItemType.OFFERING)) == false)
numItems = 1;
CharacterItemManager cim = pc.charItemManager;
@@ -155,7 +156,7 @@ public class MakeItemCmd extends AbstractDevCmd {
for (int i = 0; i < quantity; i++) {
int weight = template.item_wt;
int weight = template.getItemWt();
if (!cim.hasRoomInventory(weight)) {
throwbackError(pc, "Not enough room in inventory for any more of this item. " + i + " produced.");
@@ -169,6 +170,7 @@ public class MakeItemCmd extends AbstractDevCmd {
Item item = new Item(templateID);
item.ownerID = pc.getObjectUUID();
item.ownerType = OwnerType.PlayerCharacter;
item.containerType = ItemContainerType.INVENTORY;
if (numItems > 1)
item.setNumOfItems(numItems);
@@ -180,6 +182,11 @@ public class MakeItemCmd extends AbstractDevCmd {
return;
}
if (item == null) {
throwbackError(pc, "DB error 2: Unable to create item " + templateID + ".");
return;
}
//create prefix
if (!prefix.isEmpty())
item.addPermanentEnchantmentForDev(prefix, 0);
+3 -2
View File
@@ -11,6 +11,7 @@ package engine.devcmd.cmds;
import engine.devcmd.AbstractDevCmd;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import java.util.ArrayList;
@@ -52,8 +53,8 @@ public class PrintBankCmd extends AbstractDevCmd {
throwbackInfo(pc, "Bank for " + type + ' ' + name + " (" + tar.getObjectUUID() + ')');
for (Item item : list) {
ItemTemplate template = ItemTemplate.templates.get(item.templateID);
throwbackInfo(pc, " " + template.item_base_name + ", count: " + item.getNumOfItems());
CObjectTemplate template = CObjectTemplate.lookup(item.templateID);
throwbackInfo(pc, " " + template.getItemBaseName() + ", count: " + item.getNumOfItems());
}
Item gold = cim.getGoldBank();
+3 -2
View File
@@ -13,6 +13,7 @@ import engine.devcmd.AbstractDevCmd;
import engine.gameManager.ChatManager;
import engine.mbEnums.GameObjectType;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.effectmodifiers.AbstractEffectModifier;
public class PrintBonusesCmd extends AbstractDevCmd {
@@ -33,8 +34,8 @@ public class PrintBonusesCmd extends AbstractDevCmd {
if (target instanceof Item) {
type = "Item";
tar = (Item) target;
ItemTemplate template = ItemTemplate.templates.get(((Item) tar).templateID);
name = template.item_base_name;
CObjectTemplate template = CObjectTemplate.lookup(((Item) tar).templateID);
name = template.getItemBaseName();
} else if (target instanceof AbstractCharacter) {
tar = (AbstractCharacter) target;
name = ((AbstractCharacter) tar).getFirstName();
+48 -10
View File
@@ -10,9 +10,15 @@
package engine.devcmd.cmds;
import engine.devcmd.AbstractDevCmd;
import engine.mbEnums.GameObjectType;
import engine.objects.AbstractCharacter;
import engine.objects.AbstractGameObject;
import engine.objects.Effect;
import engine.objects.PlayerCharacter;
import engine.powers.EffectsBase;
import engine.powers.PowersBase;
import java.util.Map;
/**
*
@@ -30,28 +36,60 @@ public class PrintEffectsCmd extends AbstractDevCmd {
AbstractGameObject target) {
AbstractCharacter tar;
StringBuilder output;
String newline = "\r\n ";
Effect effect;
EffectsBase effectsBase;
PowersBase power;
if (target != null && target instanceof AbstractCharacter) {
tar = (AbstractCharacter) target;
if (target == null)
return;
String newline = "\r\n ";
String output = "Effects For Character: " + tar.getName() + newline;
if (target.getObjectType() != GameObjectType.PlayerCharacter && target.getObjectType() != GameObjectType.Mob
&& target.getObjectType() != GameObjectType.NPC)
return;
for (String effect : tar.effects.keySet()) {
output += effect + newline;
}
throwbackInfo(pc, output);
tar = (AbstractCharacter) target;
output = new StringBuilder("Effects for " + tar.getName() + " (" + tar.effects.size() + "):" + newline);
// Keys are the effect UUID for mobbase/rune effects, or the stack type (e.g. PetBuff) for power effects
for (Map.Entry<String, Effect> entry : tar.effects.entrySet()) {
effect = entry.getValue();
effectsBase = effect.getEffectsBase();
power = effect.getPower();
output.append(entry.getKey()).append(": ");
if (effectsBase != null)
output.append(effectsBase.getIDString()).append(" '").append(effectsBase.getName()).append("'");
else
output.append("no EffectsBase");
output.append(" rank ").append(effect.getTrains());
if (effect.isStatic())
output.append(", static");
else
output.append(", ").append((int) effect.getDuration()).append("s left");
if (power != null)
output.append(", from ").append(power.getIDString()).append(" '").append(power.getName()).append("'");
output.append(newline);
}
throwbackInfo(pc, output.toString());
}
@Override
protected String _getHelpString() {
return "Returns the player's current stats";
return "Lists the target's effects: key, effect id and name, rank, time left and source power";
}
@Override
protected String _getUsageString() {
return "' /printstats'";
return "' /printeffects'";
}
}
+3 -3
View File
@@ -66,7 +66,7 @@ public class PrintEquipCmd extends AbstractDevCmd {
throwbackInfo(pc, "Equip for " + type + ' ' + name + " (" + tar.getObjectUUID() + ')');
for (mbEnums.EquipSlotType slot : tarMob.charItemManager.equipped.keySet()) {
Item equip = tarMob.charItemManager.equipped.get(slot);
throwbackInfo(pc, equip.templateID + " : " + equip.template.item_base_name + ", slot: " + slot);
throwbackInfo(pc, equip.templateID + " : " + equip.template.getItemBaseName() + ", slot: " + slot);
}
return;
}
@@ -76,7 +76,7 @@ public class PrintEquipCmd extends AbstractDevCmd {
throwbackInfo(pc, "Equip for " + type + ' ' + name + " (" + tar.getObjectUUID() + ')');
for (mbEnums.EquipSlotType slot : tarMob.charItemManager.equipped.keySet()) {
Item equip = tarMob.charItemManager.equipped.get(slot);
throwbackInfo(pc, equip.templateID + " : " + equip.template.item_base_name + ", slot: " + slot);
throwbackInfo(pc, equip.templateID + " : " + equip.template.getItemBaseName() + ", slot: " + slot);
}
return;
}
@@ -87,7 +87,7 @@ public class PrintEquipCmd extends AbstractDevCmd {
for (mbEnums.EquipSlotType slot : list.keySet()) {
Item item = list.get(slot);
throwbackInfo(pc, " " + item.template.item_base_name + ", slot: " + slot);
throwbackInfo(pc, " " + item.template.getItemBaseName() + ", slot: " + slot);
}
}
@@ -12,6 +12,7 @@ package engine.devcmd.cmds;
import engine.devcmd.AbstractDevCmd;
import engine.mbEnums.ItemType;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import java.text.DecimalFormat;
import java.util.ArrayList;
@@ -75,18 +76,18 @@ public class PrintInventoryCmd extends AbstractDevCmd {
int goldCount = 0;
for (Item item : inventory) {
ItemTemplate template = ItemTemplate.templates.get(item.templateID);
CObjectTemplate template = CObjectTemplate.lookup(item.templateID);
if (item.template.item_type.equals(ItemType.GOLD) == false) {
if (item.template.getItemType().equals(ItemType.GOLD) == false) {
String chargeInfo = "";
int chargeMax = template.item_initial_charges;
int chargeMax = template.getItemInitialCharges();
if (chargeMax > 0) {
int charges = item.chargesRemaining;
chargeInfo = " charges: " + charges + '/' + chargeMax;
}
throwbackInfo(pc, " " + template.item_base_name + ", count: " + item.getNumOfItems() + chargeInfo);
throwbackInfo(pc, " " + template.getItemBaseName() + ", count: " + item.getNumOfItems() + chargeInfo);
} else
goldCount += item.getNumOfItems();
}
+3 -2
View File
@@ -11,6 +11,7 @@ package engine.devcmd.cmds;
import engine.devcmd.AbstractDevCmd;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import java.util.ArrayList;
@@ -50,8 +51,8 @@ public class PrintVaultCmd extends AbstractDevCmd {
ArrayList<Item> list = cim.getVault();
throwbackInfo(pc, "Vault for " + type + ' ' + name + " (" + tar.getObjectUUID() + ')');
for (Item item : list) {
ItemTemplate template = ItemTemplate.templates.get(item.templateID);
throwbackInfo(pc, " " + template.item_base_name + ", count: " + item.getNumOfItems());
CObjectTemplate template = CObjectTemplate.lookup(item.templateID);
throwbackInfo(pc, " " + template.getItemBaseName() + ", count: " + item.getNumOfItems());
}
Item gold = cim.getGoldVault();
if (gold != null)
+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) {
+4 -3
View File
@@ -5,6 +5,7 @@ import engine.gameManager.LootManager;
import engine.gameManager.ZoneManager;
import engine.loot.BootySetEntry;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import java.util.ArrayList;
import java.util.concurrent.ThreadLocalRandom;
@@ -34,10 +35,10 @@ public class SimulateBootyCmd extends AbstractDevCmd {
if (mob.bootySet != 0) {
for (BootySetEntry entry : LootManager._bootySetMap.get(mob.bootySet)) {
ItemTemplate template = ItemTemplate.templates.get(entry.templateID);
CObjectTemplate template = CObjectTemplate.lookup(entry.templateID);
if (template != null)
output += "[" + entry.bootyType + "] " + template.item_base_name + " [Chance] " + entry.dropChance + newline;
output += "[" + entry.bootyType + "] " + template.getItemBaseName() + " [Chance] " + entry.dropChance + newline;
}
}
@@ -57,7 +58,7 @@ public class SimulateBootyCmd extends AbstractDevCmd {
try {
mob.loadInventory();
for (Item lootItem : mob.charItemManager.getInventory()) {
switch (lootItem.template.item_type) {
switch (lootItem.template.getItemType()) {
case EMPLOYMENTCONTRACT: //CONTRACT
Contracts.add(lootItem);
break;
+3 -4
View File
@@ -118,18 +118,17 @@ public class aiInfoCmd extends AbstractDevCmd {
ArrayList<RunePowerEntry> powerEntries = new ArrayList<>(PowersManager.getPowersForRune(mob.getMobBaseID()));
// Additional powers may come from the contract ID. This is to support
// powers for player guards irrespective of the mobbase used.
// Player guards also cast their captain contract's powers, irrespective of the mobbase used.
if (mob.isPlayerGuard()) {
ArrayList<RunePowerEntry> contractEntries = new ArrayList<>();
if (mob.contract != null)
contractEntries = new ArrayList<>(PowersManager.getPowersForRune(mob.contractUUID));
contractEntries = new ArrayList<>(mbEnums.MinionType.getCaptainPowers(mob.contractUUID));
if (mob.agentType.equals(mbEnums.AIAgentType.GUARDMINION))
contractEntries = new ArrayList<>(PowersManager.getPowersForRune(mob.guardCaptain.contractUUID));
contractEntries = new ArrayList<>(mbEnums.MinionType.getCaptainPowers(mob.guardCaptain.contractUUID));
powerEntries.addAll(contractEntries);
+16 -10
View File
@@ -22,6 +22,7 @@ import engine.mbEnums.BuildingGroup;
import engine.mbEnums.GameObjectType;
import engine.net.client.msg.ErrorPopupMsg;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
@@ -30,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;
@@ -43,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) {
@@ -199,31 +202,31 @@ public enum BuildingManager {
return false;
}
ItemTemplate template = ItemTemplate.templates.get(1705032); // Elan Stone
CObjectTemplate template = CObjectTemplate.lookup(1705032); // Elan Stone
if (!player.charItemManager.hasRoomInventory(template.item_wt))
if (!player.charItemManager.hasRoomInventory(template.getItemWt()))
return false;
if (!ItemManager.MakeItemForPlayer(template.template_id, player, amount))
if (!ItemManager.MakeItemForPlayer(template.getTemplateId(), player, amount))
return false;
shrine.setFavors(0);
break;
case WAREHOUSE:
City city = building.getCity();
if (city == null)
return true;
Warehouse warehouse = city.warehouse;
Warehouse warehouse = Warehouse.destroyedWarehouses.get(building.getObjectUUID());
if (warehouse == null)
return false;
for (mbEnums.ResourceType resourceType : EnumSet.allOf(mbEnums.ResourceType.class)) {
if (!player.charItemManager.hasRoomInventory(resourceType.template.item_wt)) {
CObjectTemplate resourceTemplate = CObjectTemplate.lookup(resourceType.templateID);
if (resourceTemplate == null)
continue;
if (!player.charItemManager.hasRoomInventory(resourceTemplate.getItemWt())) {
ChatManager.chatSystemInfo(player, "You can not carry any more of that item.");
return false;
}
@@ -241,6 +244,9 @@ public enum BuildingManager {
}
}
if (Warehouse.isEmpty(warehouse))
Warehouse.destroyedWarehouses.remove(building.getObjectUUID());
break;
}
+4 -3
View File
@@ -22,6 +22,7 @@ import engine.net.client.ClientConnection;
import engine.net.client.handlers.ClientAdminCommandMsgHandler;
import engine.net.client.msg.chat.*;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.server.MBServerStatics;
import engine.server.world.WorldServer;
import org.pmw.tinylog.Logger;
@@ -535,12 +536,12 @@ public enum ChatManager {
if (item != null) //Steal
if (success) {
String name = "";
if (item.template.item_type.equals(mbEnums.ItemType.GOLD))
if (item.template.getItemType().equals(mbEnums.ItemType.GOLD))
name = amount + " gold ";
else {
String vowels = "aeiou";
ItemTemplate template = ItemTemplate.templates.get(item.templateID);
String iName = template.item_base_name;
CObjectTemplate template = CObjectTemplate.lookup(item.templateID);
String iName = template.getItemBaseName();
if (iName.length() > 0)
if (vowels.indexOf(iName.substring(0, 1).toLowerCase()) >= 0)
name = "an " + iName + ' ';
+37 -50
View File
@@ -18,6 +18,8 @@ import engine.net.client.msg.PowerProjectileMsg;
import engine.net.client.msg.TargetedActionMsg;
import engine.net.client.msg.UpdateStateMsg;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.parsers.objects.RuneTemplate;
import engine.powers.DamageShield;
import engine.powers.effectmodifiers.AbstractEffectModifier;
import engine.powers.effectmodifiers.WeaponProcEffectModifier;
@@ -26,6 +28,7 @@ import org.pmw.tinylog.Logger;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
@@ -92,25 +95,25 @@ public enum CombatManager {
return;
}
if (mainWeapon == null && offWeapon != null && !offWeapon.template.item_skill_used.equals("Block")) {
if (mainWeapon == null && offWeapon != null && !offWeapon.template.getItemSkillUsed().equals("Block")) {
//swing left hand only
processAttack(attacker, target, mbEnums.EquipSlotType.LHELD);
return;
}
if (mainWeapon == null && offWeapon != null && offWeapon.template.item_skill_used.equals("Block")) {
if (mainWeapon == null && offWeapon != null && offWeapon.template.getItemSkillUsed().equals("Block")) {
//no weapon equipped with a shield, punch with one hand
processAttack(attacker, target, mbEnums.EquipSlotType.RHELD);
return;
}
if (mainWeapon != null && offWeapon != null && offWeapon.template.item_skill_used.equals("Block")) {
if (mainWeapon != null && offWeapon != null && offWeapon.template.getItemSkillUsed().equals("Block")) {
//one weapon equipped with a shield, swing with one hand
processAttack(attacker, target, mbEnums.EquipSlotType.RHELD);
return;
}
if (mainWeapon != null && offWeapon != null && !offWeapon.template.item_skill_used.equals("Block")) {
if (mainWeapon != null && offWeapon != null && !offWeapon.template.getItemSkillUsed().equals("Block")) {
//two weapons equipped, swing both hands
processAttack(attacker, target, mbEnums.EquipSlotType.RHELD);
if (attacker.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter))
@@ -146,7 +149,7 @@ public enum CombatManager {
if (bonus != null)
rangeMod += bonus.getFloatPercentAll(mbEnums.ModType.WeaponRange, mbEnums.SourceType.None);
attackRange += weapon.template.item_weapon_max_range * rangeMod;
attackRange += engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_max_range * rangeMod;
} else if (attacker.getObjectType().equals(mbEnums.GameObjectType.Mob)) {
//unarmed mob, add the natural attack range from its rune (0 for most melee mobs)
@@ -211,7 +214,7 @@ public enum CombatManager {
if (weapon != null) {
float wepSpeed = (int) (weapon.template.item_weapon_wepspeed);
float wepSpeed = (int) (engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_wepspeed);
if (weapon.getBonusPercent(mbEnums.ModType.WeaponSpeed, mbEnums.SourceType.None) != 0f) //add weapon speed bonus
wepSpeed *= (1 + weapon.getBonus(mbEnums.ModType.WeaponSpeed, mbEnums.SourceType.None));
@@ -270,13 +273,13 @@ public enum CombatManager {
if (weapon != null && !attacker.getObjectType().equals(mbEnums.GameObjectType.Mob)) {
//check if Out of Stamina
if (attacker.getObjectType().equals(mbEnums.GameObjectType.PlayerCharacter)) {
if (attacker.getStamina() < (weapon.template.item_wt / 3f)) {
if (attacker.getStamina() < (weapon.template.getItemWt() / 3f)) {
//set auto attack job
setAutoAttackJob(attacker, slot, delay);
return;
}
}
float stam = weapon.template.item_wt / 3f;
float stam = weapon.template.getItemWt() / 3f;
stam = (stam < 1) ? 1 : stam;
attacker.modifyStamina(-(stam), attacker, true);
} else
@@ -416,7 +419,7 @@ public enum CombatManager {
if (((Mob) attacker).isSiege())
damageType = mbEnums.DamageType.SIEGE;
} else {
damageType = (mbEnums.DamageType) attacker.charItemManager.getEquipped().get(slot).template.item_weapon_damage.keySet().toArray()[0];
damageType = engine.parsers.objects.ItemTemplate.getWeapon(attacker.charItemManager.getEquipped().get(slot).templateID).weapon_damage.get(0).damage_type;
}
//get resists
@@ -577,13 +580,14 @@ public enum CombatManager {
target.setCombatTarget(attacker);
}
public static int getSwingAnimation(ItemTemplate wb, DeferredPowerJob dpj, mbEnums.EquipSlotType slot) {
public static int getSwingAnimation(CObjectTemplate wb, DeferredPowerJob dpj, mbEnums.EquipSlotType slot) {
//No weapon, return default animation
if (wb == null)
return 75;
int token;
engine.parsers.objects.ItemTemplate.Weapon weapon = engine.parsers.objects.ItemTemplate.getWeapon(wb.getTemplateId());
if (dpj != null) {
@@ -594,47 +598,30 @@ public enum CombatManager {
}
//Item has no equipment slots and should not try to return an animation, return default instead
if (wb.item_eq_slots_or == null || wb.item_eq_slots_or.isEmpty()) {
if (wb.getItemEqSlotsOr().isEmpty()) {
return 75;
}
//declare variables
int anim;
int random;
mbEnums.EquipSlotType hand;
ArrayList<int[]> anims;
//Item can only be equipped in one slot, return animation for that slot
if (wb.item_eq_slots_or.size() == 1) {
if (wb.item_eq_slots_or.iterator().next().equals(mbEnums.EquipSlotType.RHELD)) {
anim = wb.weapon_attack_anim_right.get(0)[0];
if (dpj != null) {
random = ThreadLocalRandom.current().nextInt(wb.weapon_attack_anim_right.size());
anim = wb.weapon_attack_anim_right.get(random)[0];
}
} else {
anim = wb.weapon_attack_anim_left.get(0)[0];
if (dpj != null) {
random = ThreadLocalRandom.current().nextInt(wb.weapon_attack_anim_left.size());
anim = wb.weapon_attack_anim_left.get(random)[0];
}
}
return anim;
}
//Item can only be equipped in one slot, use the animations for that slot
if (wb.getItemEqSlotsOr().size() == 1)
hand = wb.getItemEqSlotsOr().iterator().next();
else
hand = slot;
//Item can be equipped in either hand, and should have animation sets for each hand
if (slot.equals(mbEnums.EquipSlotType.RHELD)) {
anim = wb.weapon_attack_anim_right.get(0)[0];
if (dpj != null) {
random = ThreadLocalRandom.current().nextInt(wb.weapon_attack_anim_right.size());
anim = wb.weapon_attack_anim_right.get(random)[0];
}
} else {
anim = wb.weapon_attack_anim_left.get(0)[0];
if (dpj != null) {
random = ThreadLocalRandom.current().nextInt(wb.weapon_attack_anim_left.size());
anim = wb.weapon_attack_anim_left.get(random)[0];
}
}
return anim;
anims = hand.equals(mbEnums.EquipSlotType.RHELD) ? weapon.weapon_attack_anim_right : weapon.weapon_attack_anim_left;
// Many cache weapons have no swing anims; GUESS: the client falls back to the default too
if (anims.isEmpty())
return 75;
if (dpj != null)
return anims.get(ThreadLocalRandom.current().nextInt(anims.size()))[0];
return anims.get(0)[0];
}
public static int getPassiveAnimation(mbEnums.PassiveType passiveType) {
@@ -713,7 +700,7 @@ public enum CombatManager {
// Mobs use their mobbase rune, players use their race rune.
int runeID;
ItemTemplate rune;
RuneTemplate rune;
int hand = slot.equals(mbEnums.EquipSlotType.LHELD) ? 2 : 1;
switch (attacker.getObjectType()) {
@@ -727,18 +714,18 @@ public enum CombatManager {
return null;
}
rune = ItemTemplate.templates.get(runeID);
rune = RuneTemplate._runes.get(runeID);
if (rune == null || rune.rune_natural_attacks.isEmpty())
if (rune == null || rune.rune_naturalattacks.isEmpty())
return null;
for (float[] attack : rune.rune_natural_attacks)
for (float[] attack : rune.rune_naturalattacks)
if (attack.length > 3 && (int) attack[3] == hand)
return attack;
// No entry for this hand, fall back to the first natural attack
return rune.rune_natural_attacks.get(0);
return rune.rune_naturalattacks.get(0);
}
public static int calculatePetDamage(AbstractCharacter agent) {
+2 -8
View File
@@ -71,10 +71,12 @@ public enum ConfigManager {
MB_WORLD_KEYCLONE_MAX,
MB_USE_RUINS,
MB_RULESET,
MB_STATIC_BUILDINGS,
// Mobile AI modifiers
MB_AI_CAST_FREQUENCY,
MB_AI_AGGRO_RANGE,
MB_MINION_SPAWN_TIME,
//drop rates
MB_NORMAL_EXP_RATE,
@@ -165,14 +167,6 @@ public enum ConfigManager {
regex.put(MB_LOGIN_FNAME_REGEX, Pattern.compile(MB_LOGIN_FNAME_REGEX.getValue()));
Logger.info("Loading WPAK data");
// *** Needs powermanager collection
// defined before activation.
// EffectsParser.parseWpakFile();
// PowersParser.parseWpakFile();
// PowerActionParser.parseWpakFile();
return true;
}
+52 -6
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();
@@ -44,10 +49,8 @@ public enum DbManager {
// Omg refactor this out, somebody!
public static final dbCSSessionHandler CSSessionQueries = new dbCSSessionHandler();
public static final dbEnchantmentHandler EnchantmentQueries = new dbEnchantmentHandler();
public static final dbEffectsResourceCostHandler EffectsResourceCostsQueries = new dbEffectsResourceCostHandler();
public static final dbGuildHandler GuildQueries = new dbGuildHandler();
public static final dbItemHandler ItemQueries = new dbItemHandler();
public static final dbKitHandler KitQueries = new dbKitHandler();
public static final dbLootHandler LootQueries = new dbLootHandler();
public static final dbMenuHandler MenuQueries = new dbMenuHandler();
public static final dbMineHandler MineQueries = new dbMineHandler();
@@ -57,9 +60,6 @@ public enum DbManager {
public static final dbPlayerCharacterHandler PlayerCharacterQueries = new dbPlayerCharacterHandler();
public static final dbPromotionClassHandler PromotionQueries = new dbPromotionClassHandler();
public static final dbRaceHandler RaceQueries = new dbRaceHandler();
public static final dbResistHandler ResistQueries = new dbResistHandler();
public static final dbRuneBaseAttributeHandler RuneBaseAttributeQueries = new dbRuneBaseAttributeHandler();
public static final dbRuneBaseEffectHandler RuneBaseEffectQueries = new dbRuneBaseEffectHandler();
public static final dbRuneBaseHandler RuneBaseQueries = new dbRuneBaseHandler();
public static final dbSkillBaseHandler SkillsBaseQueries = new dbSkillBaseHandler();
public static final dbSkillReqHandler SkillReqQueries = new dbSkillReqHandler();
@@ -69,13 +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 dbPowerHandler PowerQueries = new dbPowerHandler();
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;
@@ -101,6 +101,7 @@ public enum DevCmdManager {
DevCmdManager.registerDevCmd(new SetAdminRuneCmd());
DevCmdManager.registerDevCmd(new SetInvulCmd());
DevCmdManager.registerDevCmd(new MakeItemCmd());
DevCmdManager.registerDevCmd(new FillCmd());
DevCmdManager.registerDevCmd(new SetSubRaceCmd());
// Admin
+21 -9
View File
@@ -15,8 +15,8 @@ import engine.mbEnums;
import engine.net.client.msg.ItemProductionMsg;
import engine.objects.City;
import engine.objects.Item;
import engine.objects.ItemTemplate;
import engine.objects.NPC;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.EffectsBase;
import org.pmw.tinylog.Logger;
@@ -49,6 +49,9 @@ public enum ForgeManager implements Runnable {
public static final ConcurrentHashMap<NPC, ConcurrentHashMap.KeySetView<WorkOrder, Boolean>> vendorWorkOrderLookup = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<Item, WorkOrder> itemWorkOrderLookup = new ConcurrentHashMap<>();
// Server-only data from static_vendor_items; the client templates have no mod table.
public static HashMap<Integer, Integer> vendorModTables = new HashMap<>();
public static void start() {
Thread forgeManager;
@@ -128,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();
@@ -222,12 +230,12 @@ public enum ForgeManager implements Runnable {
// Calculate production cost for a single run of the workOrder
HashMap<mbEnums.ResourceType, Integer> production_cost = new HashMap<>();
ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);
CObjectTemplate template = CObjectTemplate.lookup(workOrder.templateID);
// Add gold and resource costs from template
production_cost.put(mbEnums.ResourceType.GOLD, template.item_value);
production_cost.putAll(template.item_resource_cost);
production_cost.put(mbEnums.ResourceType.GOLD, template.getItemValue());
production_cost.putAll(template.getItemResourceCosts());
// Calculate cost of prefix and suffix
@@ -248,8 +256,8 @@ public enum ForgeManager implements Runnable {
// Create new virtual item from specified template
ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);
Item forgedItem = new Item(workOrder.templateID);
int modTable = getVendorModTable(workOrder.templateID);
// forgedItem gets a negative id; a virtual item which is not persisted
@@ -264,13 +272,13 @@ public enum ForgeManager implements Runnable {
// Assign a prefix and suffix to this item if random rolled
if (workOrder.prefixToken == 0 && workOrder.vendor.getItemModTable().contains((template.modTable)))
forgedItem.prefixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.PREFIX, template.modTable);
if (workOrder.prefixToken == 0 && workOrder.vendor.getItemModTable().contains(modTable))
forgedItem.prefixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.PREFIX, modTable);
else
forgedItem.prefixToken = workOrder.prefixToken;
if (workOrder.suffixToken == 0 && workOrder.vendor.getItemModTable().contains((template.modTable)))
forgedItem.suffixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.SUFFIX, template.modTable);
if (workOrder.suffixToken == 0 && workOrder.vendor.getItemModTable().contains(modTable))
forgedItem.suffixToken = calcRandomMod(workOrder.vendor, mbEnums.ItemModType.SUFFIX, modTable);
else
forgedItem.suffixToken = workOrder.suffixToken;
@@ -364,6 +372,10 @@ public enum ForgeManager implements Runnable {
DbManager.WarehouseQueries.WRITE_WORKORDER(workOrder);
}
public static int getVendorModTable(int templateID) {
return vendorModTables.getOrDefault(templateID, 0);
}
public static int calcRandomMod(NPC vendor, mbEnums.ItemModType itemModType, int modTable) {
// Random prefix or suffix token based on item.template.modtable
+1 -1
View File
@@ -307,7 +307,7 @@ public enum GroupManager {
return false;
}
if (!item.template.item_type.equals(mbEnums.ItemType.GOLD)) //only split goldItem
if (!item.template.getItemType().equals(mbEnums.ItemType.GOLD)) //only split goldItem
return false;
Group group = getGroup(pc);
+100
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,95 @@ 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<>();
// 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);
for (int guildKey : seedEntry.guildKeys)
if (_npcGuildsByKey.put(guildKey, guild) != null || guildSeed.noGuildKeys.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);
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);
}
+37 -55
View File
@@ -2,6 +2,7 @@ package engine.gameManager;
import engine.mbEnums;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.EffectsBase;
import org.pmw.tinylog.Logger;
@@ -19,68 +20,49 @@ 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.
public static Boolean ValidRace(Item item, mbEnums.MonsterType race) {
if (item.template.item_race_req.isEmpty() && item.template.item_race_res.isEmpty())
EnumSet<mbEnums.MonsterType> races = item.template.getItemRaceReq();
boolean restrict = item.template.getItemRaceReqRestrict();
if (races.isEmpty())
return true;
if (item.template.item_race_req.isEmpty() == false)
if (item.template.item_race_req.contains(race))
return true;
if (item.template.item_race_res.isEmpty() == false)
if (item.template.item_race_res.contains(race) == false)
return true;
return false;
return races.contains(race) != restrict;
}
public static Boolean ValidClass(Item item, mbEnums.ClassType base, mbEnums.ClassType profession) {
// Early exit if no entry
EnumSet<mbEnums.ClassType> classes = item.template.getItemClassReq();
boolean restrict = item.template.getItemClassReqRestrict();
boolean listed;
if (item.template.item_class_req.isEmpty() && item.template.item_class_res.isEmpty())
if (classes.isEmpty())
return true;
if (item.template.item_class_req.isEmpty() == false)
if (item.template.item_class_req.contains(base) || item.template.item_class_req.contains(profession))
return true;
listed = classes.contains(base) || classes.contains(profession);
if (item.template.item_class_res.isEmpty() == false)
if (item.template.item_class_res.contains(base) == false && item.template.item_class_res.contains(profession) == false)
return true;
return false;
return listed != restrict;
}
public static Boolean ValidDiscipline(Item item, EnumSet<mbEnums.DisciplineType> discs) {
// Early exit if no entry
EnumSet<mbEnums.DisciplineType> disciplines = item.template.getItemDiscReq();
boolean restrict = item.template.getItemDiscReqRestrict();
EnumSet<mbEnums.DisciplineType> workSet;
if (item.template.item_disc_req.isEmpty() && item.template.item_disc_res.isEmpty())
if (disciplines.isEmpty())
return true;
EnumSet<mbEnums.DisciplineType> workSet = EnumSet.copyOf(discs);
workSet = EnumSet.copyOf(discs);
workSet.retainAll(disciplines);
if (item.template.item_disc_req.isEmpty() == false) {
workSet.retainAll(item.template.item_disc_req);
if (workSet.isEmpty() == false)
return true;
}
if (item.template.item_disc_res.isEmpty() == false) {
workSet.retainAll(item.template.item_disc_res);
if (workSet.isEmpty() == false)
return true;
}
return false;
return workSet.isEmpty() == restrict;
}
public static Boolean canCharacterEquip(Item item, AbstractCharacter character) {
@@ -91,12 +73,12 @@ public enum ItemManager {
CharacterSkill characterSkill;
if (item.template.item_skill_required.isEmpty())
if (item.template.getItemSkillReq().isEmpty())
return true;
for (String skillRequired : item.template.item_skill_required.keySet()) {
for (String skillRequired : item.template.getItemSkillReq().keySet()) {
int required_value = item.template.item_skill_required.get(skillRequired);
int required_value = item.template.getItemSkillReq().get(skillRequired);
characterSkill = skills.get(skillRequired);
if (characterSkill == null)
@@ -112,31 +94,31 @@ public enum ItemManager {
public static boolean isTwoHanded(Item item) {
if (!item.template.item_type.equals(mbEnums.ItemType.WEAPON))
if (!item.template.getItemType().equals(mbEnums.ItemType.WEAPON))
return false;
return item.template.item_eq_slots_and.contains(EnumSet.of(mbEnums.EquipSlotType.LHELD, mbEnums.EquipSlotType.RHELD));
return item.template.getItemEqSlotsAnd().containsAll(EnumSet.of(mbEnums.EquipSlotType.LHELD, mbEnums.EquipSlotType.RHELD));
}
public static boolean isTwoHanded(ItemTemplate template) {
public static boolean isTwoHanded(CObjectTemplate template) {
if (!template.item_type.equals(mbEnums.ItemType.WEAPON))
if (!template.getItemType().equals(mbEnums.ItemType.WEAPON))
return false;
return template.item_eq_slots_and.contains(EnumSet.of(mbEnums.EquipSlotType.LHELD, mbEnums.EquipSlotType.RHELD));
return template.getItemEqSlotsAnd().containsAll(EnumSet.of(mbEnums.EquipSlotType.LHELD, mbEnums.EquipSlotType.RHELD));
}
public static boolean isShield(Item item) {
if (item.template.item_skill_required.containsKey("Block"))
if (item.template.getItemSkillReq().containsKey("Block"))
return true;
return false;
}
public static boolean isShield(ItemTemplate template) {
public static boolean isShield(CObjectTemplate template) {
if (template.item_skill_required.containsKey("Block"))
if (template.getItemSkillReq().containsKey("Block"))
return true;
return false;
@@ -150,7 +132,7 @@ public enum ItemManager {
return false;
//Item not valid for slot
if (item.template.item_eq_slots_or.contains(slot) == false)
if (item.template.getItemEqSlotsOr().contains(slot) == false)
return false;
// Slot is taken
@@ -165,7 +147,7 @@ public enum ItemManager {
(slot == mbEnums.EquipSlotType.RHELD && equipped.get(mbEnums.EquipSlotType.LHELD) != null)))
return false;
if (item.template.item_type.equals(mbEnums.ItemType.WEAPON))
if (item.template.getItemType().equals(mbEnums.ItemType.WEAPON))
if (equipped.get(slot) != null && equipped.get(slot).equals(item) == false)
return false;
@@ -197,7 +179,7 @@ public enum ItemManager {
//players can't wear 0 value items.
return item.template.item_value != 0 || Kit.IsNoobGear(item.templateID);
return item.template.getItemValue() != 0 || Kit.IsNoobGear(item.templateID);
}
+9 -8
View File
@@ -13,6 +13,7 @@ import engine.mbEnums;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.chat.ChatSystemMsg;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
@@ -91,8 +92,8 @@ public enum LootManager {
//lastly, check mobs inventory for godly or disc runes to send a server announcement
for (Item item : mob.getInventory()) {
if ((item.templateID > 2499 && item.templateID <= 3050) || item.template.item_base_name.toLowerCase().contains("of the gods")) {
ChatSystemMsg chatMsg = new ChatSystemMsg(null, mob.getName() + " in " + mob.getParentZone().zoneName + " has found the " + item.template.item_base_name + ". Are you tough enough to take it?");
if ((item.templateID > 2499 && item.templateID <= 3050) || item.template.getItemBaseName().toLowerCase().contains("of the gods")) {
ChatSystemMsg chatMsg = new ChatSystemMsg(null, mob.getName() + " in " + mob.getParentZone().zoneName + " has found the " + item.template.getItemBaseName() + ". Are you tough enough to take it?");
chatMsg.setMessageType(10);
chatMsg.setChannel(mbEnums.ChatChannelType.SYSTEM.getChannelID());
DispatchManager.dispatchMsgToAll(chatMsg);
@@ -179,12 +180,12 @@ public enum LootManager {
if (itemUUID == 0)
return null;
if (ItemTemplate.templates.get(itemUUID).item_type.equals(mbEnums.ItemType.RESOURCE)) {
if (CObjectTemplate.lookup(itemUUID).getItemType().equals(mbEnums.ItemType.RESOURCE)) {
int amount = ThreadLocalRandom.current().nextInt(tableRow.minSpawn, tableRow.maxSpawn + 1);
return new MobLoot(mob, ItemTemplate.templates.get(itemUUID), amount, false);
return new MobLoot(mob, CObjectTemplate.lookup(itemUUID), amount, false);
}
outItem = new MobLoot(mob, ItemTemplate.templates.get(itemUUID), false);
outItem = new MobLoot(mob, CObjectTemplate.lookup(itemUUID), false);
if (selectedRow.pModTable != 0)
try {
@@ -374,7 +375,7 @@ public enum LootManager {
if (chanceRoll > bse.dropChance)
return;
MobLoot lootItem = new MobLoot(mob, ItemTemplate.templates.get(bse.templateID), true);
MobLoot lootItem = new MobLoot(mob, CObjectTemplate.lookup(bse.templateID), true);
if (lootItem != null)
mob.charItemManager.addItemToInventory(lootItem);
@@ -422,14 +423,14 @@ public enum LootManager {
//create the item from the table, quantity is always 1
MobLoot winnings = new MobLoot(playerCharacter, ItemTemplate.templates.get(selectedItem.templateID), 1, false);
MobLoot winnings = new MobLoot(playerCharacter, CObjectTemplate.lookup(selectedItem.templateID), 1, false);
if (winnings == null)
return;
//early exit if the inventory of the player will not old the item
if (itemMan.hasRoomInventory(winnings.template.item_wt) == false) {
if (itemMan.hasRoomInventory(winnings.template.getItemWt()) == false) {
ErrorPopupMsg.sendErrorPopup(playerCharacter, 21);
return;
}
+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) {
+74 -34
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;
@@ -19,19 +18,19 @@ import engine.net.client.ClientConnection;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.PetMsg;
import engine.objects.*;
import engine.parsers.objects.CObjectTemplate;
import engine.powers.EffectsBase;
import engine.powers.RuneSkillAdjustEntry;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
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;
@@ -225,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
@@ -315,26 +315,26 @@ public enum NPCManager {
//has main hand weapon
Item weapon = guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD);
if (weapon.template.item_primary_attr.equals(mbEnums.AttributeType.Strength))
if (engine.parsers.objects.ItemTemplate._items.get(weapon.templateID).item_primary_attr == mbEnums.AttributeType.Strength)
primaryStat = guard.getStatStrCurrent();
else
primaryStat = guard.getStatDexCurrent();
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + weapon.template.item_weapon_damage.values().iterator().next()[0]) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + weapon.template.item_weapon_damage.values().iterator().next()[1]) * rankModifier) + primaryStat;
guard.speedHandOne = weapon.template.item_weapon_wepspeed;
guard.rangeHandOne = weapon.template.item_weapon_max_range;
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_min) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_max) * rankModifier) + primaryStat;
guard.speedHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_wepspeed;
guard.rangeHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_max_range;
} else if (guard.charItemManager.equipped.containsKey(mbEnums.EquipSlotType.LHELD) && !ItemManager.isShield(guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template)) {
//has off hand weapon
Item weapon = guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD);
if (weapon.template.item_primary_attr.equals(mbEnums.AttributeType.Strength))
if (engine.parsers.objects.ItemTemplate._items.get(weapon.templateID).item_primary_attr == mbEnums.AttributeType.Strength)
primaryStat = guard.getStatStrCurrent();
else
primaryStat = guard.getStatDexCurrent();
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + weapon.template.item_weapon_damage.values().iterator().next()[0]) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + weapon.template.item_weapon_damage.values().iterator().next()[1]) * rankModifier) + primaryStat;
guard.speedHandOne = weapon.template.item_weapon_wepspeed;
guard.rangeHandOne = weapon.template.item_weapon_max_range;
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_min) * rankModifier) + primaryStat;
guard.maxDamageHandOne = (int) ((guard.mobBase.getDamageMax() + engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_damage.get(0).damage_max) * rankModifier) + primaryStat;
guard.speedHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_wepspeed;
guard.rangeHandOne = engine.parsers.objects.ItemTemplate.getWeapon(weapon.templateID).weapon_max_range;
} else {
primaryStat = guard.getStatStrCurrent();
guard.minDamageHandOne = (int) ((guard.mobBase.getDamageMin()) * rankModifier) + primaryStat;
@@ -352,8 +352,8 @@ public enum NPCManager {
int baseDef = guard.mobBase.getDefenseRating();
int armorDefense = 0;
for (Item equipped : guard.charItemManager.equipped.values())
if (equipped.template.item_type.equals(mbEnums.ItemType.ARMOR) || ItemManager.isShield(equipped.template))
armorDefense += equipped.template.item_defense_rating;
if (equipped.template.getItemType().equals(mbEnums.ItemType.ARMOR) || ItemManager.isShield(equipped.template))
armorDefense += engine.parsers.objects.ItemTemplate._items.get(equipped.templateID).item_defense_rating;
guard.defenseRating = dexterity + baseDef + armorDefense;
}
@@ -361,9 +361,9 @@ public enum NPCManager {
int strength = guard.getStatStrCurrent();
int baseAtr = guard.mobBase.getAttackRating();
if (guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD) != null)
guard.atrHandOne = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.item_skill_required.values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.item_skill_required.values().toArray()[0] * 3));
guard.atrHandOne = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.getItemSkillReq().values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.RHELD).template.getItemSkillReq().values().toArray()[0] * 3));
else if (guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD) != null && !ItemManager.isShield(guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template))
guard.atrHandTwo = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.item_skill_required.values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.item_skill_required.values().toArray()[0] * 3));
guard.atrHandTwo = baseAtr + (int) ((strength * 0.5f) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.getItemSkillReq().values().toArray()[0] * 4) + ((int) guard.charItemManager.equipped.get(mbEnums.EquipSlotType.LHELD).template.getItemSkillReq().values().toArray()[0] * 3));
else
guard.atrHandOne = baseAtr;
}
@@ -434,28 +434,63 @@ public enum NPCManager {
}
}
if (mob.isNecroPet())
applyNecroPetStats(mob);
}
// No client data grants NECROPETSTATS, yet NEC-018 and DRU-066 require it on the target; the server adds it to necro pets
private static void applyNecroPetStats(Mob mob) {
EffectsBase necroPetStats = PowersManager.getEffectByIDString("NECROPETSTATS");
String effectKey;
Effect currentEffect;
int rank = -1;
if (necroPetStats == null) {
Logger.error("NECROPETSTATS effect not loaded");
return;
}
// GUESS: rank follows the pet's own level ladder (PET_NECRO / PET_NECRO_HARDHIT, from reqLvl 9), as the old DB rows did
for (MobBaseEffects mbe : mob.mobBase.effectsList)
if (mbe.getReqLvl() > 0 && mbe.getReqLvl() <= (int) mob.level)
rank = Math.max(rank, mbe.getRank());
if (rank < 0)
return;
effectKey = Integer.toString(necroPetStats.getUUID());
currentEffect = mob.effects.get(effectKey);
if (currentEffect != null) {
if (currentEffect.getTrains() >= rank)
return;
currentEffect.cancelJob();
}
mob.addEffectNoTimer(effectKey, necroPetStats, rank, true);
}
public static void applyEquipmentResists(Mob mob) {
EnumMap<mbEnums.DamageType, Float> armorResists;
if (mob.charItemManager.equipped.isEmpty())
return;
for (Item equipped : mob.charItemManager.equipped.values()) {
if (equipped.template.item_type.equals(mbEnums.ItemType.ARMOR)) {
mob.resists.setResist(mbEnums.DamageType.SLASHING, mob.resists.getResist(mbEnums.DamageType.SLASHING, 0) + equipped.template.combat_attack_resist.get("SLASHING"));
mob.resists.setResist(mbEnums.DamageType.CRUSHING, mob.resists.getResist(mbEnums.DamageType.CRUSHING, 0) + equipped.template.combat_attack_resist.get("CRUSHING"));
mob.resists.setResist(mbEnums.DamageType.PIERCING, mob.resists.getResist(mbEnums.DamageType.PIERCING, 0) + equipped.template.combat_attack_resist.get("PIERCING"));
if (equipped.template.getItemType().equals(mbEnums.ItemType.ARMOR)) {
armorResists = equipped.template.getCombatAttackResist();
mob.resists.setResist(mbEnums.DamageType.SLASHING, mob.resists.getResist(mbEnums.DamageType.SLASHING, 0) + armorResists.getOrDefault(mbEnums.DamageType.SLASHING, 0f));
mob.resists.setResist(mbEnums.DamageType.CRUSHING, mob.resists.getResist(mbEnums.DamageType.CRUSHING, 0) + armorResists.getOrDefault(mbEnums.DamageType.CRUSHING, 0f));
mob.resists.setResist(mbEnums.DamageType.PIERCING, mob.resists.getResist(mbEnums.DamageType.PIERCING, 0) + armorResists.getOrDefault(mbEnums.DamageType.PIERCING, 0f));
}
}
}
public static void applyMobbaseSkill(Mob mob) {
SkillsBase baseSkill = DbManager.SkillsBaseQueries.GET_BASE_BY_TOKEN(mob.mobBase.getMobBaseStats().getBaseSkill());
if (baseSkill != null)
mob.getSkills().put(baseSkill.getName(), new CharacterSkill(baseSkill, mob, mob.mobBase.getMobBaseStats().getBaseSkillAmount()));
}
public static void applyRuneSkills(Mob mob, int runeID) {
//load mob skill adjustments from mobbase rune
if (PowersManager._allRuneSkillAdjusts.containsKey(runeID))
@@ -466,7 +501,7 @@ public enum NPCManager {
if (skillBase == null)
continue;
if (entry.level <= mob.level)
if (mob.skills.containsKey(entry.name) == false)
if (mob.skills.containsKey(entry.skill_type) == false)
mob.skills.put(entry.skill_type, new CharacterSkill(skillBase, mob, entry.rank));
else
mob.skills.put(entry.skill_type, new CharacterSkill(skillBase, mob, entry.rank + mob.skills.get(entry.skill_type).getNumTrains()));
@@ -480,6 +515,11 @@ public enum NPCManager {
npc.runes.add(shopkeeper);
if (NPCManager._runeSetMap.containsKey(npc.runeSetID)) {
for (int runeID : _runeSetMap.get(npc.runeSetID)) {
// Shopkeeper is already applied above and most rune sets repeat it
if (runeID == 252620)
continue;
RuneBase rb = RuneBase.getRuneBase(runeID);
if (rb != null) {
CharacterRune toApply = new CharacterRune(rb, npc.getObjectUUID());
@@ -536,14 +576,14 @@ public enum NPCManager {
public static long calcRollingDuration(NPC vendor, int templateID) {
ItemTemplate template = ItemTemplate.templates.get(templateID);
CObjectTemplate template = CObjectTemplate.lookup(templateID);
float rollingDuration;
if (template == null)
return 0;
if (template.item_bane_rank > 0)
return (long) (template.item_bane_rank * 60 * 60 * 3 * 1000 * Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue()));
if (template.getItemBaneRank() > 0)
return (long) (template.getItemBaneRank() * 60 * 60 * 3 * 1000 * Float.parseFloat(ConfigManager.MB_PRODUCTION_RATE.getValue()));
if (vendor.building == null)
return 600;
+240 -65
View File
@@ -10,9 +10,6 @@ package engine.gameManager;
import engine.InterestManagement.Terrain;
import engine.InterestManagement.WorldGrid;
import engine.db.handlers.dbEffectsBaseHandler;
import engine.db.handlers.dbPowerHandler;
import engine.db.handlers.dbSkillReqHandler;
import engine.job.AbstractJob;
import engine.job.AbstractScheduleJob;
import engine.job.JobContainer;
@@ -28,8 +25,12 @@ import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.*;
import engine.objects.*;
import engine.parsers.objects.wpak.EffectTemplate;
import engine.parsers.objects.wpak.PowActionCostTemplate;
import engine.parsers.objects.wpak.PowerActionTemplate;
import engine.parsers.objects.wpak.PowerTemplate;
import engine.powers.*;
import engine.powers.poweractions.AbstractPowerAction;
import engine.powers.poweractions.*;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
@@ -37,6 +38,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
@@ -53,7 +55,6 @@ public enum PowersManager {
public static HashMap<String, AbstractPowerAction> powerActionsByIDString = new HashMap<>();
public static HashMap<Integer, AbstractPowerAction> powerActionsByID = new HashMap<>();
public static HashMap<String, Integer> ActionTokenByIDString = new HashMap<>();
public static HashMap<String, Integer> AnimationOverrides = new HashMap<>();
public static HashMap<Integer, ArrayList<RunePowerEntry>> _allRunePowers;
public static HashMap<Integer, ArrayList<RuneSkillAdjustEntry>> _allRuneSkillAdjusts;
public static HashMap<String, HashMap<ResourceType, Integer>> _effect_costMaps = new HashMap<>();
@@ -90,16 +91,200 @@ public enum PowersManager {
return PowersManager.effectsBaseByToken.get(token);
}
// This pre-loads only PowersBase for login
public static void InitializeLoginPowers() {
public static void loadEffectCostMaps() {
// get all PowersBase
ArrayList<PowersBase> pbList = dbSkillReqHandler.getAllPowersBase();
for (PowActionCostTemplate costTemplate : PowActionCostTemplate._costs.values())
PowersManager._effect_costMaps.put(costTemplate.id, new HashMap<>(costTemplate.costs));
for (PowersBase pb : pbList) {
if (pb.getToken() != 0)
PowersManager.powersBaseByToken.put(pb.getToken(), pb);
Logger.info("Loaded " + PowersManager._effect_costMaps.size() + " effect cost maps from PowActionCostInfo.cfg");
}
// Effects.cfg in file order, then the ApplyEffect/ApplyEffects/Invis actions with no Effects.cfg block
private static void loadEffects() {
int effectID = 0;
int actionKeyCount = 0;
PowerActionTemplate powerActionTemplate;
for (EffectTemplate effectTemplate : EffectTemplate._effects.values())
addEffect(new EffectsBase(effectTemplate, ++effectID));
for (String actionID : new TreeSet<>(PowerActionTemplate._powerActions.keySet())) {
if (EffectTemplate._effects.containsKey(actionID))
continue;
powerActionTemplate = PowerActionTemplate._powerActions.get(actionID);
switch (powerActionTemplate.action_type) {
case ApplyEffect:
case ApplyEffects:
case Invis:
addEffect(new EffectsBase(powerActionTemplate, ++effectID));
actionKeyCount++;
break;
}
}
Logger.info("Loaded " + EffectTemplate._effects.size() + " effects from Effects.cfg and "
+ actionKeyCount + " effect keys from PowerActions.cfg");
}
// PowerActions.cfg in id order; ids 1..n, OPENGATE keeps 5000
private static void loadPowerActions() {
int powerActionID = 0;
PowerActionTemplate powerActionTemplate;
AbstractPowerAction powerAction;
AbstractPowerAction openGateAction;
for (String actionID : new TreeSet<>(PowerActionTemplate._powerActions.keySet())) {
powerActionTemplate = PowerActionTemplate._powerActions.get(actionID);
powerAction = buildPowerAction(powerActionTemplate, ++powerActionID);
if (powerAction == null) {
Logger.error("Power action " + actionID + " has unsupported type " + powerActionTemplate.action_type);
continue;
}
PowersManager.ActionTokenByIDString.put(actionID, DbManager.hasher.SBStringHash(actionID));
PowersManager.powerActionsByIDString.put(actionID, powerAction);
PowersManager.powerActionsByID.put(powerAction.getUUID(), powerAction);
}
openGateAction = new OpenGatePowerAction(5000, "OPENGATE", "OpenGate", false);
PowersManager.powerActionsByIDString.put("OPENGATE", openGateAction);
PowersManager.powerActionsByID.put(openGateAction.getUUID(), openGateAction);
Logger.info("Loaded " + PowersManager.powerActionsByIDString.size() + " power actions from PowerActions.cfg (incl. OPENGATE)");
}
private static AbstractPowerAction buildPowerAction(PowerActionTemplate powerActionTemplate, int id) {
HashMap<String, EffectsBase> effects = PowersManager.effectsBaseByIDString;
switch (powerActionTemplate.action_type) {
case ApplyEffect:
return new ApplyEffectPowerAction(powerActionTemplate, id, effects);
case ApplyEffects:
return new ApplyEffectsPowerAction(powerActionTemplate, id, effects);
case Block:
return new BlockPowerAction(powerActionTemplate, id);
case Charm:
// FEAR and CONFUSION Charm actions stay no-ops, as when the DB typed them Confusion
if (powerActionTemplate.effect_ids.get(0).equals("CHARM"))
return new CharmPowerAction(powerActionTemplate, id);
return new ConfusionPowerAction(powerActionTemplate, id);
case ClaimMine:
return new ClaimMinePowerAction(powerActionTemplate, id);
case ClearAggro:
return new ClearAggroPowerAction(powerActionTemplate, id);
case ClearNearbyAggro:
return new ClearNearbyAggroPowerAction(powerActionTemplate, id);
case Confusion:
return new ConfusionPowerAction(powerActionTemplate, id);
case CreateMob:
return new CreateMobPowerAction(powerActionTemplate, id);
case DamageOverTime:
return new DamageOverTimePowerAction(powerActionTemplate, id, effects);
case DeferredPower:
return new DeferredPowerPowerAction(powerActionTemplate, id, effects);
case DirectDamage:
return new DirectDamagePowerAction(powerActionTemplate, id, effects);
case Invis:
return new InvisPowerAction(powerActionTemplate, id, effects);
case MobRecall:
return new MobRecallPowerAction(powerActionTemplate, id);
case Peek:
return new PeekPowerAction(powerActionTemplate, id);
case Recall:
return new RecallPowerAction(powerActionTemplate, id);
case RemoveEffect:
return new RemoveEffectPowerAction(powerActionTemplate, id);
case Resurrect:
return new ResurrectPowerAction(powerActionTemplate, id);
case RunegateTeleport:
return new RunegateTeleportPowerAction(powerActionTemplate, id);
case SetItemFlag:
return new SetItemFlagPowerAction(powerActionTemplate, id);
case SimpleDamage:
return new SimpleDamagePowerAction(powerActionTemplate, id);
case SpireDisable:
return new SpireDisablePowerAction(powerActionTemplate, id);
case Steal:
return new StealPowerAction(powerActionTemplate, id);
case Summon:
return new SummonPowerAction(powerActionTemplate, id);
case Teleport:
return new TeleportPowerAction(powerActionTemplate, id);
case Track:
return new TrackPowerAction(powerActionTemplate, id, effects);
case TransferStat:
return new TransferStatPowerAction(powerActionTemplate, id, effects);
case TransferStatOT:
return new TransferStatOTPowerAction(powerActionTemplate, id, effects);
case Transform:
return new TransformPowerAction(powerActionTemplate, id, effects);
case TreeChoke:
return new TreeChokePowerAction(powerActionTemplate, id);
default:
return null;
}
}
private static void addEffect(EffectsBase effectsBase) {
PowersManager.effectsBaseByToken.put(effectsBase.getToken(), effectsBase);
PowersManager.effectsBaseByIDString.put(effectsBase.getIDString(), effectsBase);
}
// Powers.cfg in file order, ids 1..n (the old static_power_powerbase IDs)
private static void loadPowers() {
int powerID = 0;
PowersBase powersBase;
for (PowerTemplate powerTemplate : PowerTemplate._powers.values()) {
powersBase = new PowersBase(powerTemplate, ++powerID);
PowersManager.powersBaseByIDString.put(powersBase.getIDString(), powersBase);
PowersManager.powersBaseByToken.put(powersBase.getToken(), powersBase);
}
Logger.info("Loaded " + PowersManager.powersBaseByIDString.size() + " powers from Powers.cfg");
}
// ACTION= lines in Powers.cfg order, ids 1..n (offset in ActionsBase), then the OPENGATE actions
private static void loadActionLists() {
int actionID = 0;
PowersBase powersBase;
AbstractPowerAction powerAction;
for (PowerTemplate powerTemplate : PowerTemplate._powers.values()) {
powersBase = PowersManager.powersBaseByIDString.get(powerTemplate.power_id);
for (PowerTemplate.ActionEntry actionEntry : powerTemplate.actions) {
powerAction = PowersManager.powerActionsByIDString.get(actionEntry.action_id);
if (powerAction == null) {
Logger.error("Power " + powerTemplate.power_id + " lists unknown action " + actionEntry.action_id);
continue;
}
powersBase.getActions().add(new ActionsBase(actionEntry, powerTemplate.power_id, ++actionID, powerAction));
}
}
ActionsBase.addOpenGateActions(PowersManager.powersBaseByIDString);
Logger.info("Loaded " + actionID + " power action lines from Powers.cfg");
}
// The login server needs only the token map
public static void InitializeLoginPowers() {
loadPowers();
}
public static ArrayList<RunePowerEntry> getPowersForRune(int rune_id) {
@@ -115,48 +300,13 @@ public enum PowersManager {
// This pre-loads all powers and effects
public static void InitializePowers() {
// Add EffectsBase
ArrayList<EffectsBase> ebList = dbEffectsBaseHandler.getAllEffectsBase();
loadEffects();
for (EffectsBase eb : ebList) {
PowersManager.effectsBaseByToken.put(eb.getToken(), eb);
PowersManager.effectsBaseByIDString.put(eb.getIDString(), eb);
loadPowerActions();
}
// Add Fail Conditions
EffectsBase.getFailConditions(PowersManager.effectsBaseByIDString);
// Add Modifiers to Effects
dbEffectsBaseHandler.cacheAllEffectModifiers();
// Add Source Types to Effects
dbPowerHandler.addAllSourceTypes();
dbPowerHandler.addAllAnimationOverrides();
// Add PowerActions
AbstractPowerAction.getAllPowerActions(PowersManager.powerActionsByIDString, PowersManager.powerActionsByID, PowersManager.effectsBaseByIDString);
// Load valid Item Flags
// AbstractPowerAction.loadValidItemFlags(PowersManager.powerActionsByIDString);
// get all PowersBase
ArrayList<PowersBase> pbList = dbSkillReqHandler.getAllPowersBase();
for (PowersBase pb : pbList) {
if (pb.getToken() != 0) {
PowersManager.powersBaseByIDString.put(pb.getIDString(), pb);
PowersManager.powersBaseByToken.put(pb.getToken(), pb);
}
}
// Add Power Prereqs
PowerPrereq.getAllPowerPrereqs(PowersManager.powersBaseByIDString);
// Add Fail Conditions
dbSkillReqHandler.getFailConditions(PowersManager.powersBaseByIDString);
// Add Actions Base
ActionsBase.getActionsBase(PowersManager.powersBaseByIDString,
PowersManager.powerActionsByIDString);
loadPowers();
loadActionLists();
}
public static EffectsBase setEffectToken(int ID, int token) {
@@ -1255,6 +1405,27 @@ public enum PowersManager {
return false;
}
// MONSTERTYPERESTRICTS: mobs use their race rune's monster type, players their RaceType's
private static boolean isRestrictedMonsterType(AbstractWorldObject target, PowersBase pb) {
MonsterType monsterType = null;
PlayerCharacter player;
if (target == null || pb.monsterTypeRestrictions.isEmpty())
return false;
if (target.getObjectType() == GameObjectType.Mob && ((Mob) target).getMobBase() != null)
monsterType = ((Mob) target).getMobBase().monsterType;
else if (target.getObjectType() == GameObjectType.PlayerCharacter) {
player = (PlayerCharacter) target;
if (player.race != null && player.race.getRaceType() != null)
monsterType = player.race.getRaceType().getMonsterType();
}
return monsterType != null && pb.monsterTypeRestrictions.contains(monsterType);
}
public static void sendRecyclePower(int token, ClientConnection origin) {
RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(token);
@@ -1580,9 +1751,6 @@ public enum PowersManager {
//Pet only
allTargets = WorldGrid.getObjectsInRangePartial(
targetLoc, pb.getRadius(), MBServerStatics.MASK_PET);
else if (pb.targetNecroPet())
allTargets = WorldGrid.getObjectsInRangePartialNecroPets(
targetLoc, pb.getRadius());
else
allTargets = WorldGrid.getObjectsInRangePartial(
targetLoc, pb.getRadius(), 0);
@@ -1694,6 +1862,11 @@ public enum PowersManager {
continue;
}
if (isRestrictedMonsterType(awo, pb)) {
awolist.remove();
continue;
}
if (awo != null && awo.getObjectType().equals(GameObjectType.PlayerCharacter)) {
// Remove players who are in safe mode
@@ -1779,9 +1952,6 @@ public enum PowersManager {
//Pet only
allTargets = WorldGrid.getObjectsInRangePartial(
targetLoc, pb.getRadius(), MBServerStatics.MASK_PET);
else if (pb.targetNecroPet())
allTargets = WorldGrid.getObjectsInRangePartialNecroPets(
targetLoc, pb.getRadius());
else
allTargets = WorldGrid.getObjectsInRangePartial(
targetLoc, pb.getRadius(), 0);
@@ -1846,6 +2016,11 @@ public enum PowersManager {
continue;
}
if (isRestrictedMonsterType(awo, pb)) {
awolist.remove();
continue;
}
if (awo != null && awo.getObjectType().equals(GameObjectType.PlayerCharacter)) {
// Remove players who are in safe mode
PlayerCharacter pcc = (PlayerCharacter) awo;
@@ -2238,14 +2413,14 @@ public enum PowersManager {
String outmsg = "Invalid Target";
ChatManager.chatSystemInfo(pc, outmsg);
return false; // can't target player, stop here
} // target is mob
} // target is pet; pets also carry MASK_MOB, so test them first. A pet is still a Mobile (client 0x49E440)
else if ((target.getObjectTypeMask() & MBServerStatics.MASK_PET) != 0)
return pb.targetPet() || pb.targetMob();
// target is mob
else if ((target.getObjectTypeMask() & MBServerStatics.MASK_MOB) != 0)
return pb.targetMob();
// target is pet
else if ((target.getObjectTypeMask() & MBServerStatics.MASK_PET) != 0)
return pb.targetPet();
// target is Building
else if ((target.getObjectTypeMask() & MBServerStatics.MASK_BUILDING) != 0)
return pb.targetBuilding();
@@ -2255,12 +2430,12 @@ public enum PowersManager {
if (pb.targetItem())
return true;
// TODO add these checks later
else if (pb.targetArmor() && item.template.item_type.equals(ItemType.ARMOR))
else if (pb.targetArmor() && item.template.getItemType().equals(ItemType.ARMOR))
return true;
else if (pb.targetJewelry() && item.template.item_type.equals(ItemType.JEWELRY))
else if (pb.targetJewelry() && item.template.getItemType().equals(ItemType.JEWELRY))
return true;
else
return pb.targetWeapon() && item.template.item_type.equals(ItemType.WEAPON);
return pb.targetWeapon() && item.template.getItemType().equals(ItemType.WEAPON);
} // How did we get here? all valid targets have been covered
else
return false;
@@ -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);
}
}
+305
View File
@@ -0,0 +1,305 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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"},
{"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) + "}";
}
}
}
+90 -2
View File
@@ -17,7 +17,9 @@ 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.CZoneTemplate;
import engine.parsers.objects.ZoneTreeEntry;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
@@ -36,7 +38,10 @@ 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 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 +56,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) {
+1 -5
View File
@@ -82,11 +82,7 @@ public class ChantJob extends AbstractEffectJob {
}
if (AbstractWorldObject.IsAbstractCharacter(source))
//handle invul
if (power.getUUID() != 334)
((AbstractCharacter) this.source).setLastChant((int) (this.power.getChantDuration()) * 1000, this);
else
((AbstractCharacter) this.source).setLastChant((int) (this.power.getChantDuration()) * 1000, this);
((AbstractCharacter) this.source).setLastChant((int) (this.power.getChantDuration()) * 1000, this);
} else {
PowersManager.finishEffectTime(this.source, this.target, this.action, this.trains);
if (AbstractWorldObject.IsAbstractCharacter(source)) {
+5 -2
View File
@@ -25,6 +25,7 @@ public class DamageOverTimeJob extends AbstractEffectJob {
private final DamageOverTimePowerAction dot;
private int iteration = 0;
private int liveCounter = 0;
private final int tickLength;
public DamageOverTimeJob(AbstractWorldObject source, AbstractWorldObject target, String stackType, int trains, ActionsBase action, PowersBase power, EffectsBase eb, DamageOverTimePowerAction dot) {
super(source, target, stackType, trains, action, power, eb);
@@ -33,7 +34,9 @@ public class DamageOverTimeJob extends AbstractEffectJob {
if (this.target != null && AbstractWorldObject.IsAbstractCharacter(target))
this.liveCounter = ((AbstractCharacter) target).getLiveCounter();
this.iteration = action.getDurationInSeconds(trains) / this.dot.getNumIterations();
// PowerActions.cfg gives the tick count; the ticks split the action's duration
this.iteration = this.dot.getNumIterations();
this.tickLength = action.getDuration(trains) / Math.max(1, this.dot.getNumIterations());
}
@Override
@@ -82,7 +85,7 @@ public class DamageOverTimeJob extends AbstractEffectJob {
}
public int getTickLength() {
return this.dot.getNumIterations() * 1000;
return this.tickLength;
}
public int inc() {
+6 -2
View File
@@ -21,11 +21,15 @@ public class TransferStatOTJob extends AbstractEffectJob {
private final TransferStatOTPowerAction dot;
private int iteration = 0;
private final int tickLength;
public TransferStatOTJob(AbstractWorldObject source, AbstractWorldObject target, String stackType, int trains, ActionsBase action, PowersBase power, EffectsBase eb, TransferStatOTPowerAction dot) {
super(source, target, stackType, trains, action, power, eb);
this.dot = dot;
this.iteration = action.getDurationInSeconds(trains) / this.dot.getNumIterations();
// PowerActions.cfg gives the tick count; the ticks split the action's duration
this.iteration = this.dot.getNumIterations();
this.tickLength = action.getDuration(trains) / Math.max(1, this.dot.getNumIterations());
}
@Override
@@ -64,7 +68,7 @@ public class TransferStatOTJob extends AbstractEffectJob {
}
public int getTickLength() {
return this.dot.getNumIterations() * 1000;
return this.tickLength;
}
public int inc() {
+4 -4
View File
@@ -12,9 +12,9 @@ import engine.gameManager.DbManager;
import engine.gameManager.ForgeManager;
import engine.mbEnums;
import engine.objects.Item;
import engine.objects.ItemTemplate;
import engine.objects.NPC;
import engine.objects.Warehouse;
import engine.parsers.objects.CObjectTemplate;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -127,16 +127,16 @@ public class WorkOrder implements Delayed {
int validation_result = 0;
ItemTemplate template = ItemTemplate.templates.get(workOrder.templateID);
CObjectTemplate template = CObjectTemplate.lookup(workOrder.templateID);
if (workOrder.vendor.getBuilding() == null)
return 58; //58: The formula is beyond the means of this facility
if (!workOrder.vendor.charItemManager.hasRoomInventory(template.item_wt))
if (!workOrder.vendor.charItemManager.hasRoomInventory(template.getItemWt()))
return 30; //30: That person cannot carry that item
if ((workOrder.prefixToken != 0 || workOrder.suffixToken != 0) &&
!workOrder.vendor.getItemModTable().contains((template.modTable)))
!workOrder.vendor.getItemModTable().contains(ForgeManager.getVendorModTable(workOrder.templateID)))
return 59; //59: This hireling does not have this formula
if (!Warehouse.calcCostOverrun(workOrder).isEmpty())
+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);
-30
View File
@@ -583,36 +583,6 @@ final public class FastMath {
return ret.normalize();
}
/**
* Turns a String into an Integer hash that the client will recognize.
*
* @param s The String to hash
* @return the Integer hash
*/
public static int hash(String s) {
int out = 0, pad = 0;
for (char c : s.toCharArray()) {
if (c == ' ' || c == '\\') {
pad += 5;
continue;
}
if (c > 0x40 && c < 0x60)
out ^= _rotl((byte) 0x60, pad);
out ^= _rotl((byte) (c & 0xDF), pad);
pad += 5;
}
return out;
}
/**
* Utility function for making hashes
*/
private static int _rotl(int value, int shift) {
if ((shift &= 31) == 0)
return value;
return (value << shift) | (value >> (32 - shift));
}
/**
* Gets number of seconds until next hours
*/
+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);
}
}
+143 -40
View File
@@ -15,10 +15,10 @@ import engine.math.Vector2f;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.objects.AbstractCharacter;
import engine.objects.ItemTemplate;
import engine.objects.Shrine;
import engine.objects.Zone;
import engine.powers.EffectsBase;
import engine.powers.RunePowerEntry;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
@@ -824,6 +824,7 @@ public class mbEnums {
ACID,
DISEASE,
UNKNOWN,
ANTISIEGE,
// these added for immunities
ATTACK,
POWERS,
@@ -842,34 +843,26 @@ public class mbEnums {
public static DamageType getDamageType(String modName) {
String name;
if (modName.isEmpty())
return DamageType.NONE;
if(modName.toLowerCase().equals("blind"))
modName = "BLINDNESS";
// Client and DB strings vary in case and punctuation ("Fire", "Anti-Siege")
if(modName.toLowerCase().equals("powerblock"))
modName = "POWERINHIBITOR";
name = modName.replaceAll("[\\s,-]", "").toUpperCase();
//validity check for not looking up damage type that doesn't exist
boolean valid = false;
for(DamageType type : DamageType.values()){
if(type.name().equals(modName))
valid = true;
}
if (name.equals("BLIND"))
name = "BLINDNESS";
if(!valid)
return DamageType.NONE;
if (name.equals("POWERBLOCK"))
name = "POWERINHIBITOR";
DamageType damageType;
for (DamageType type : DamageType.values())
if (type.name().equals(name))
return type;
try {
damageType = DamageType.valueOf(modName.replace(",", "").toUpperCase());
} catch (Exception e) {
Logger.error(e);
return DamageType.NONE;
}
return damageType;
return DamageType.NONE;
}
}
@@ -1104,11 +1097,28 @@ public class mbEnums {
public static EffectSourceType GetEffectSourceType(String modName) {
EffectSourceType returnMod;
String name;
if (modName.isEmpty())
return EffectSourceType.None;
name = modName.replace(",", "").replace("-", "").trim();
// Effects.cfg SOURCE blocks use the short damage names
switch (name) {
case "Crush":
name = "Crushing";
break;
case "Pierce":
name = "Piercing";
break;
case "Slash":
name = "Slashing";
break;
}
try {
returnMod = EffectSourceType.valueOf(modName.replace(",", ""));
returnMod = EffectSourceType.valueOf(name);
} catch (Exception e) {
Logger.error(e);
return EffectSourceType.None;
@@ -1138,6 +1148,7 @@ public class mbEnums {
BMHealing,
Charm,
ClassBoon,
ColdDeBuff,
Confusion,
DamageAbsorber,
DamageDebuff,
@@ -1152,6 +1163,7 @@ public class mbEnums {
ElementalDeBuff,
EnchantWeapon,
Fear,
FireDeBuff,
Flight,
Frenzy,
GroupHeal,
@@ -1159,12 +1171,16 @@ public class mbEnums {
HealOverTime,
HealResBuff,
HealthPotion,
HolyDeBuff,
IgnoreStack,
Invisible,
LightningDeBuff,
MagicDeBuff,
ManaPotion,
MangonelFire,
MeleeDamageDeBuff,
MeleeDeBuff,
MentalDeBuff,
MoveBuff,
MoveDebuff,
NoFear,
@@ -1177,6 +1193,9 @@ public class mbEnums {
NoTrack,
PassiveDefense,
PersAttrSPRBuff,
PersonalAttrCONBuff,
PersonalAttrDEXBuff,
PersonalAttrSTRBuff,
PetBuff,
PierceResistanceDebuff,
PoisonBuchinine,
@@ -1199,7 +1218,6 @@ public class mbEnums {
RecoveryStaminaDeBuff,
ResistanceBuff,
ResistanceDeBuff,
ResistanceDebuff,
Root,
SafeMode,
SelfOneAttrBuff,
@@ -1223,6 +1241,7 @@ public class mbEnums {
Stun,
Track,
Transform,
UnholyDeBuff,
WeaponMove;
public static StackType GetStackType(String modName) {
@@ -1509,6 +1528,15 @@ public class mbEnums {
}
}
// rune_type values from the client cache data (COBJECTS/RUNE)
public enum RuneType {
CLASS,
DISCIPLINE,
RACE,
TALENT
}
public enum ShrineType {
Aelfborn(1701900, -75506007, true),
@@ -2632,55 +2660,61 @@ public class mbEnums {
public enum MinionType {
AELFBORNGUARD(951, 1637, MinionClass.MELEE, "Guard", "Aelfborn"),
AELFBORNMAGE(952, 1635, MinionClass.MAGE, "Adept", "Aelfborn"),
AELFBORNMAGE(952, 1635, MinionClass.MAGE, "Adept", "Aelfborn", "WIZ-016", "WIZ-061", "WIZ-025", "WIZ-020"),
AMAZONGUARD(1500, 1670, MinionClass.MELEE, "Guard", "Amazon"),
AMAZONMAGE(1502, 1638, MinionClass.MAGE, "Fury", "Amazon"),
AMAZONMAGE(1502, 1638, MinionClass.MAGE, "Fury", "Amazon", "FUR-003", "FUR-011", "FUR-009", "FUR-005"),
ARACOIXGUARD(1600, 1672, MinionClass.MELEE, "Guard", "Aracoix"), //used guard captain equipset.
ARACOIXMAGE(1602, 885, MinionClass.MAGE, "Adept", "Aracoix"),
ARACOIXMAGE(1602, 885, MinionClass.MAGE, "Adept", "Aracoix", "WLR-029", "WLR-019", "WLR-027", "WLR-012"),
CENTAURGUARD(1650, 1642, MinionClass.MELEE, "Guard", "Centaur"),
CENTAURMAGE(1652, 1640, MinionClass.MAGE, "Druid", "Centaur"),
CENTAURMAGE(1652, 1640, MinionClass.MAGE, "Druid", "Centaur", "PRL-017", "PRL-046", "PRL-033", "PRL-009"),
DWARVENARCHER(845, 1644, MinionClass.ARCHER, "Marksman", "Dwarven"),
DWARVENGUARD(1050, 1666, MinionClass.MELEE, "Guard", "Dwarven"),
DWARVENMAGE(1052, 1643, MinionClass.MAGE, "War Priest", "Dwarven"),
DWARVENMAGE(1052, 1643, MinionClass.MAGE, "War Priest", "Dwarven", "PRL-017", "PRL-046", "PRL-033", "PRL-009"),
ELFGUARD(1180, 1671, MinionClass.MELEE, "Guard", "Elven"), //old 1645
ELFMAGE(1182, 1667, MinionClass.MAGE, "Adept", "Elven"),
ELFMAGE(1182, 1667, MinionClass.MAGE, "Adept", "Elven", "WIZ-020", "WIZ-025", "WIZ-016", "WIZ-061"),
FORESTGUARD(1550, 1668, MinionClass.MELEE, "Guard", "Forest"), //captain changed to guard equipset
FORESTMAGE(1552, 436, MinionClass.MAGE, "Adept", "Forest"),
FORESTMAGE(1552, 436, MinionClass.MAGE, "Adept", "Forest", "DRU-009", "DRU-023", "DRU-036", "DRU-015"),
HOLYGUARD(1525, 1658, MinionClass.MELEE, "Guard", "Holy Church"),
HOLYMAGE(1527, 1646, MinionClass.MAGE, "Prelate", "Holy Church"),
HOLYMAGE(1527, 1646, MinionClass.MAGE, "Prelate", "Holy Church", "PRL-017", "PRL-046", "PRL-033", "PRL-009"),
HUMANARCHER(846, 1654, MinionClass.ARCHER, "Archer", "Human"),
HUMANGUARD(840, 1665, MinionClass.MELEE, "Guard", "Human"),
HUMANMAGE(848, 1655, MinionClass.MAGE, "Adept", "Human"),
HUMANMAGE(848, 1655, MinionClass.MAGE, "Adept", "Human", "WIZ-016", "WIZ-025", "WIZ-020", "WIZ-061"),
IREKEIGUARD(1350, 1659, MinionClass.MELEE, "Guard", "Irekei"),
IREKEIMAGE(1352, 1660, MinionClass.MAGE, "Adept", "Irekei"),
IREKEIMAGE(1352, 1660, MinionClass.MAGE, "Adept", "Irekei", "WIZ-061", "WIZ-016", "WIZ-025", "WIZ-020"),
MINOTAURARCHER(1701, 0, MinionClass.ARCHER, "Archer", "Minotaur"),
MINOTAURGUARD(1700, 1673, MinionClass.MELEE, "Guard", "Minotaur"),
NORTHMANGUARD(1250, 1669, MinionClass.MELEE, "Guard", "Northman"),
NORTHMANMAGE(1252, 1650, MinionClass.MAGE, "Runecaster", "Northman"),
NORTHMANMAGE(1252, 1650, MinionClass.MAGE, "Runecaster", "Northman", "WIZ-061", "WIZ-016", "WIZ-025", "WIZ-020"),
SHADEGUARD(1450, 1662, MinionClass.MELEE, "Guard", "Shade"),
SHADEMAGE(1452, 1664, MinionClass.MAGE, "Adept", "Shade"),
SHADEMAGE(1452, 1664, MinionClass.MAGE, "Adept", "Shade", "NEC-001", "NEC-003", "MAG-013", "MAG-025"),
TEMPLARGUARD(841, 1564, MinionClass.MELEE, "Marksman", "Templar"),
TEMPLEGUARD(1575, 1652, MinionClass.MELEE, "Guard", "Temple"),
TEMPLEMAGE(1577, 1656, MinionClass.MAGE, "Confessor", "Temple"),
TEMPLEMAGE(1577, 1656, MinionClass.MAGE, "Confessor", "Temple", "CON-048", "CON-015", "CON-012", "CON-051"),
UNDEADGUARD(980100, 1674, MinionClass.MELEE, "Guard", "Undead"),
UNDEADMAGE(980102, 1675, MinionClass.MAGE, "Adept", "Undead"),
UNDEADMAGE(980102, 1675, MinionClass.MAGE, "Adept", "Undead", "NEC-001", "NEC-003", "MAG-013", "MAG-025"),
WEREWOLFGUARD(980104, 0, MinionClass.MELEE, "Guard", "Werewolf"),
WEREBEARGUARD(980103, 0, MinionClass.MELEE, "Guard", "Werebear");
public static HashMap<Integer, MinionType> ContractToMinionMap = new HashMap<>();
private static final int CAPTAIN_POWER_RANK = 40;
private final int captainContractID;
private final int equipSetID;
private final MinionClass minionClass;
private final String name;
private final String race;
private final ArrayList<RunePowerEntry> captainPowers;
MinionType(int captainContractID, int equipSetID, MinionClass minionClass, String name, String race) {
// Captain powers were hand-made by the server team; no client rune or deed lists them
MinionType(int captainContractID, int equipSetID, MinionClass minionClass, String name, String race, String... captainPowerTypes) {
this.captainContractID = captainContractID;
this.equipSetID = equipSetID;
this.minionClass = minionClass;
this.name = name;
this.race = race;
this.captainPowers = new ArrayList<>();
for (String powerType : captainPowerTypes)
this.captainPowers.add(new RunePowerEntry(race + " " + name, powerType, CAPTAIN_POWER_RANK));
}
public static void InitializeMinions() {
@@ -2689,6 +2723,17 @@ public class mbEnums {
ContractToMinionMap.put(minionType.captainContractID, minionType);
}
// The powers a guard captain's contract casts, shared by its minions; empty for non-caster contracts
public static ArrayList<RunePowerEntry> getCaptainPowers(int captainContractID) {
MinionType minionType = ContractToMinionMap.get(captainContractID);
if (minionType == null)
return new ArrayList<>();
return minionType.captainPowers;
}
public String getName() {
return name;
}
@@ -2761,7 +2806,6 @@ public class mbEnums {
public static HashMap<Integer, ResourceType> templateHashLookup = new HashMap<>();
public static HashMap<Integer, ResourceType> resourceHashLookup = new HashMap<>();
public int templateID;
public ItemTemplate template;
public int resourceHash;
public int templateHash;
public int deposit_limit;
@@ -2769,7 +2813,6 @@ public class mbEnums {
ResourceType(int templateID, int resourceHash, int templateHash, int deposit_limit, int mine_production) {
this.templateID = templateID;
this.template = ItemTemplate.templates.get(this.templateID);
this.resourceHash = resourceHash;
this.templateHash = templateHash;
this.deposit_limit = deposit_limit;
@@ -2908,6 +2951,14 @@ public class mbEnums {
EMPTY_NEWITEM;
}
// Cache JSON names. UNKNOWN is raw 0, which the client treats as no slot (sb.exe 0x4AC810).
public enum SheathSlot {
UNKNOWN,
BACK,
WAIST,
NONE
}
public enum AttributeType {
None,
Strength,
@@ -3096,6 +3147,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),
+29 -33
View File
@@ -19,6 +19,7 @@ import engine.mobileAI.Threads.ReSpawner;
import engine.mobileAI.utilities.MovementUtilities;
import engine.net.client.msg.PerformActionMsg;
import engine.objects.*;
import engine.parsers.objects.RuneTemplate;
import engine.powers.ActionsBase;
import engine.powers.PowersBase;
import engine.powers.RunePowerEntry;
@@ -49,6 +50,8 @@ public class MobAI {
private static final boolean PET_AGGRO_ENABLED = false;
private static final int STANDING_GUARD_RUNE_ID = 252623;
private static void attackTarget(Mob mob, AbstractWorldObject target) {
try {
@@ -276,7 +279,7 @@ public class MobAI {
// Mobile has no powers defined in mobbase or contract..
if (PowersManager.getPowersForRune(mob.getMobBaseID()).isEmpty() && PowersManager.getPowersForRune(contractID).isEmpty())
if (PowersManager.getPowersForRune(mob.getMobBaseID()).isEmpty() && mbEnums.MinionType.getCaptainPowers(contractID).isEmpty())
return false;
if (mob.nextCastTime == 0)
@@ -309,18 +312,17 @@ public class MobAI {
powerEntries = new ArrayList<>(PowersManager.getPowersForRune(mob.getMobBaseID()));
purgeEntries = new ArrayList<>();
// Additional powers may come from the contract ID. This is to support
// powers for player guards irrespective of the mobbase used.
// Player guards also cast their captain contract's powers, irrespective of the mobbase used.
if (mob.isPlayerGuard()) {
ArrayList<RunePowerEntry> contractEntries = new ArrayList<>();
if (mob.contract != null)
contractEntries = PowersManager.getPowersForRune(mob.contractUUID);
contractEntries = mbEnums.MinionType.getCaptainPowers(mob.contractUUID);
if (mob.agentType.equals(mbEnums.AIAgentType.GUARDMINION))
contractEntries = PowersManager.getPowersForRune(mob.guardCaptain.contractUUID);
contractEntries = mbEnums.MinionType.getCaptainPowers(mob.guardCaptain.contractUUID);
powerEntries.addAll(contractEntries);
@@ -357,7 +359,7 @@ public class MobAI {
int powerRank = runePowerEntry.rank;
if (mob.isPlayerGuard())
powerRank = getGuardPowerRank(mob);
powerRank = getGuardPowerRank(mob, mobPower);
// Cast the spell
@@ -387,33 +389,27 @@ public class MobAI {
return false;
}
public static int getGuardPowerRank(Mob mob) {
int powerRank = 1;
// Client power rank adds every POWERSADJ step at or below the level (0x4B34B0). The Standing Guard rune's
// Wizard steps are the curve for every captain spell, capped at MaxLevel as player casts are.
public static int getGuardPowerRank(Mob mob, PowersBase power) {
switch (mob.getRank()) {
case 1:
powerRank = 10;
break;
case 2:
powerRank = 15;
break;
case 3:
powerRank = 20;
break;
case 4:
powerRank = 25;
break;
case 5:
powerRank = 30;
break;
case 6:
powerRank = 35;
break;
case 7:
powerRank = 40;
break;
}
return powerRank;
RuneTemplate guardRune = RuneTemplate._runes.get(STANDING_GUARD_RUNE_ID);
ArrayList<int[]> rankSteps;
int powerRank = 0;
if (guardRune == null || guardRune.item_power_action.isEmpty())
return 1;
rankSteps = guardRune.item_power_action.get(power.getIDString());
if (rankSteps == null)
rankSteps = guardRune.item_power_action.values().iterator().next();
for (int[] rankStep : rankSteps)
if (rankStep[1] <= mob.getLevel())
powerRank += rankStep[0];
return Math.max(1, Math.min(powerRank, power.getMaxTrains()));
}
public static void mobCallForHelp(Mob mob) {
@@ -470,7 +466,7 @@ public class MobAI {
//trebuchet spawn handler
if (mob.despawned && mob.getMobBase().getLoadID() == 13171) {
if (mob.despawned && mob.getMobBase().getObjectUUID() == 13171) {
checkForRespawn(mob);
return;
}
@@ -46,7 +46,7 @@ public class ActivateNPCMsgHandler extends AbstractClientMsgHandler {
// Filter hirelings by slot type
for (Item hirelings : player.getInventory()) {
if (hirelings.template.item_type.equals(ItemType.EMPLOYMENTCONTRACT)) {
if (hirelings.template.getItemType().equals(ItemType.EMPLOYMENTCONTRACT)) {
contract = DbManager.ContractQueries.GET_CONTRACT(hirelings.templateID);
@@ -78,7 +78,7 @@ public class AddItemToTradeWindowMsgHandler extends AbstractClientMsgHandler {
if (!item.validForInventory(source.getClientConnection(), source, source.charItemManager))
return false;
if (!tradingWith.hasRoomTrade(item.template.item_wt)) {
if (!tradingWith.hasRoomTrade(item.template.getItemWt())) {
dispatch = Dispatch.borrow(source, msg);
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY);
return false;
@@ -97,9 +97,13 @@ public class DestroyBuildingHandler extends AbstractClientMsgHandler {
if (city == null)
return true;
DbManager.WarehouseQueries.DELETE_WAREHOUSE(city.warehouse);
Warehouse.destroyedWarehouses.remove(building.getObjectUUID());
city.warehouse = null;
// Rubble no longer owns city.warehouse, which may be null or a newly placed warehouse
if (city.warehouse != null && building.getRank() != -1) {
DbManager.WarehouseQueries.DELETE_WAREHOUSE(city.warehouse);
city.warehouse = null;
}
}
@@ -24,6 +24,7 @@ import engine.net.client.msg.guild.GuildInfoMsg;
import engine.objects.Guild;
import engine.objects.Item;
import engine.objects.PlayerCharacter;
import engine.parsers.objects.DeedTemplate;
import engine.util.StringUtils;
public class GuildCreationFinalizeHandler extends AbstractClientMsgHandler {
@@ -42,6 +43,7 @@ public class GuildCreationFinalizeHandler extends AbstractClientMsgHandler {
Guild playerGuild;
Item charter;
DeedTemplate charterDeed;
Dispatch dispatch;
msg = (GuildCreationFinalizeMsg) baseMsg;
@@ -65,7 +67,9 @@ public class GuildCreationFinalizeHandler extends AbstractClientMsgHandler {
// Item must be a valid charterType (type 10 in db)
if (charter == null || (charter.template.item_type.equals(ItemType.CHARTER) == false)) {
charterDeed = DeedTemplate._deeds.get(charter.templateID);
if (charterDeed == null || (charterDeed.item_type.equals(ItemType.CHARTER) == false)) {
ErrorPopupMsg.sendErrorPopup(player, GuildManager.NO_CHARTER_FOUND);
return true;
}
@@ -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) {
@@ -238,10 +244,10 @@ public class ItemProductionMsgHandler extends AbstractClientMsgHandler {
if (targetItem == null)
return;
if (targetItem.template.item_type.equals(ItemType.GOLD))
if (targetItem.template.getItemType().equals(ItemType.GOLD))
return;
if (!vendor.charItemManager.hasRoomInventory(targetItem.template.item_wt)) {
if (!vendor.charItemManager.hasRoomInventory(targetItem.template.getItemWt())) {
ErrorPopupMsg.sendErrorPopup(player, 21);
return;
}
@@ -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)
@@ -319,7 +330,7 @@ public class ItemProductionMsgHandler extends AbstractClientMsgHandler {
if (targetItem == null)
continue;
if (targetItem.template.item_type.equals(ItemType.GOLD))
if (targetItem.template.getItemType().equals(ItemType.GOLD))
return;
if (!vendor.charItemManager.doesCharOwnThisItem(targetItem.getObjectUUID()))
@@ -328,14 +339,14 @@ public class ItemProductionMsgHandler extends AbstractClientMsgHandler {
if (!vendor.charItemManager.inventoryContains(targetItem))
continue;
itemValue = targetItem.template.item_value;
itemValue = targetItem.template.getItemValue();
if (vendor.getBuilding().getStrongboxValue() + itemValue > vendor.getBuilding().getMaxGold()) {
ErrorPopupMsg.sendErrorPopup(player, 201);
break;
}
switch (targetItem.template.item_type) {
switch (targetItem.template.getItemType()) {
case EMPLOYMENTCONTRACT:
case CHARTER:
case DEED:
@@ -465,13 +476,13 @@ public class ItemProductionMsgHandler extends AbstractClientMsgHandler {
if (targetItem == null)
return;
if (targetItem.template.item_type.equals(ItemType.GOLD))
if (targetItem.template.getItemType().equals(ItemType.GOLD))
return;
if (!vendor.charItemManager.inventoryContains(targetItem))
return;
if (!player.charItemManager.hasRoomInventory(targetItem.template.item_wt))
if (!player.charItemManager.hasRoomInventory(targetItem.template.getItemWt()))
return;
player.charItemManager.buyFromNPC(targetItem, vendor);
@@ -161,7 +161,7 @@ public class LootMsgHandler extends AbstractClientMsgHandler {
if (itemRet == null)
return true;
if (item.template.item_type.equals(mbEnums.ItemType.GOLD)) {
if (item.template.getItemType().equals(mbEnums.ItemType.GOLD)) {
// this is done to prevent the temporary goldItem item
// (from the mob) from appearing in player's inventory.
// It also updates the goldItem quantity display
@@ -187,7 +187,7 @@ public class LootMsgHandler extends AbstractClientMsgHandler {
//TODO send group loot message if player is grouped and visible
Group group = GroupManager.getGroup(player);
if (group != null && group.getSplitGold() && (item.template.item_type.equals(mbEnums.ItemType.GOLD) == false)) {
if (group != null && group.getSplitGold() && (item.template.getItemType().equals(mbEnums.ItemType.GOLD) == false)) {
String name = item.getName();
String text = player.getFirstName() + " has looted " + name + '.';
ChatManager.chatGroupInfoCanSee(player, text);
@@ -202,7 +202,7 @@ public class LootMsgHandler extends AbstractClientMsgHandler {
if (itemRet == null)
return true;
if (item.template.item_type.equals(mbEnums.ItemType.GOLD)) {
if (item.template.getItemType().equals(mbEnums.ItemType.GOLD)) {
// this is done to prevent the temporary goldItem item
// (from the mob) from appearing in player's inventory.
// It also updates the goldItem quantity display
@@ -241,7 +241,7 @@ public class LootMsgHandler extends AbstractClientMsgHandler {
//TODO send group loot message if player is grouped and visible
Group group = GroupManager.getGroup(player);
if (group != null && group.getSplitGold() && (item.template.item_type.equals(mbEnums.ItemType.GOLD) == false)) {
if (group != null && group.getSplitGold() && (item.template.getItemType().equals(mbEnums.ItemType.GOLD) == false)) {
String name = item.getName();
String text = player.getFirstName() + " has looted " + name + '.';
ChatManager.chatGroupInfoCanSee(player, text);
@@ -141,17 +141,9 @@ public class ManageCityAssetMsgHandler extends AbstractClientMsgHandler {
return true;
}
City city = building.getCity();
Warehouse warehouse = Warehouse.destroyedWarehouses.get(building.getObjectUUID());
if (city == null)
return true;
Warehouse warehouse = city.warehouse;
if (warehouse == null)
return true;
if (Warehouse.isEmpty(warehouse)) {
if (warehouse == null || Warehouse.isEmpty(warehouse)) {
ErrorPopupMsg.sendErrorPopup(player, 167); // no more resources.
return true;
}
@@ -8,6 +8,7 @@ import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.*;
import engine.objects.*;
import engine.parsers.objects.DeedTemplate;
import engine.powers.PowersBase;
import java.util.ArrayList;
@@ -78,7 +79,7 @@ public class ObjectActionMsgHandler extends AbstractClientMsgHandler {
int uuid = item.templateID;
switch (item.template.item_type) {
switch (item.template.getItemType()) {
case DEED: //buildings
//Call add building screen here, ib.getUseID() get's building ID
@@ -99,7 +100,7 @@ public class ObjectActionMsgHandler extends AbstractClientMsgHandler {
pam.setX(loc.getX() + 64); //offset grid from tol
pam.setY(loc.getY());
pam.setZ(loc.getZ() + 64); //offset grid from tol
pam.addPlacementInfo(item.template.deed_structure_id);
pam.addPlacementInfo(DeedTemplate._deeds.get(item.templateID).deed_structure_id);
dispatch = Dispatch.borrow(player, pam);
DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.SECONDARY);
@@ -187,14 +188,14 @@ public class ObjectActionMsgHandler extends AbstractClientMsgHandler {
if (uuid > 680069 && uuid < 680074) //Handle Charter, Deed, Petition, Warrant here
break;
if (item.template.item_bane_rank > 0) {
if (item.template.getItemBaneRank() > 0) {
// Only one banestone at a time
lock.writeLock().lock();
try {
if (Bane.summonBanestone(player, origin, item.template.item_bane_rank) == true)
if (Bane.summonBanestone(player, origin, item.template.getItemBaneRank()) == true)
itemMan.consume(item);
} finally {
lock.writeLock().unlock();
@@ -225,7 +226,7 @@ public class ObjectActionMsgHandler extends AbstractClientMsgHandler {
// Bypass for waterbuckets
if (item.template.item_type.equals(mbEnums.ItemType.BUCKET)) {
if (item.template.getItemType().equals(mbEnums.ItemType.BUCKET)) {
// test for valid target type
if (target.getObjectType() == mbEnums.GameObjectType.PlayerCharacter)
@@ -250,10 +251,9 @@ public class ObjectActionMsgHandler extends AbstractClientMsgHandler {
}
if (waterbucketBypass == false) {
String powerString = item.template.item_power_grant.keySet().iterator().next();
PowersBase powerAction = PowersManager.powersBaseByIDString.get(powerString);
int powerValue = item.template.item_power_grant.get(powerString);
PowersManager.applyPower(player, target, Vector3fImmutable.ZERO, powerAction.getToken(), powerValue, true);
engine.parsers.objects.PowerGrant powerGrant = item.template.getItemPowerGrant().get(0);
PowersBase powerAction = PowersManager.powersBaseByIDString.get(powerGrant.power_type);
PowersManager.applyPower(player, target, Vector3fImmutable.ZERO, powerAction.getToken(), powerGrant.power_value, true);
}
itemMan.consume(item);
} else //just remove the item at this point
@@ -266,7 +266,8 @@ public class ObjectActionMsgHandler extends AbstractClientMsgHandler {
case RUNE:
if(ApplyRuneMsg.applyRune(uuid, origin, player)) {
itemMan.consume(item);
}
} else
ErrorPopupMsg.sendErrorPopup(player, 161);
break;
default: //shouldn't be here, consume item
dispatch = Dispatch.borrow(player, msg);
@@ -18,6 +18,7 @@ import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.PlaceAssetMsg;
import engine.net.client.msg.PlaceAssetMsg.PlacementInfo;
import engine.objects.*;
import engine.parsers.objects.DeedTemplate;
import engine.server.MBServerStatics;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger;
@@ -135,6 +136,8 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
private static boolean validateBuildingPlacement(Zone serverZone, PlaceAssetMsg msg, ClientConnection origin, PlayerCharacter player, PlacementInfo placementInfo) {
Bane bane;
if (serverZone.guild_zone == false) {
PlaceAssetMsg.sendPlaceAssetError(origin, 52, player.getName());
return false;
@@ -205,6 +208,15 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
return false;
}
// Siege equipment has its own validation and never reaches here
bane = city.getBane();
if (bane != null) {
PlaceAssetMsg.sendPlaceAssetError(origin, 74, ""); //"Only buildings of war may be placed during a bane"
return false;
}
if (placementCollisionCheck(serverZone, origin, placementInfo)) {
PlaceAssetMsg.sendPlaceAssetError(origin, 3, ""); // Conflict between proposed assets
return false;
@@ -345,8 +357,8 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
Item contract = null;
for (Item inventoryItem : playerCharacter.getInventory()) {
if (inventoryItem.template.item_type.equals(ItemType.DEED) &&
inventoryItem.template.deed_structure_id == buildingList.getBlueprintUUID()) {
if (inventoryItem.template.getItemType().equals(ItemType.DEED) &&
DeedTemplate._deeds.get(inventoryItem.templateID).deed_structure_id == buildingList.getBlueprintUUID()) {
contract = inventoryItem;
break;
}
@@ -1168,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
@@ -1291,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) {
@@ -1330,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");
@@ -1341,6 +1353,7 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
}
Warehouse warehouse = new Warehouse(warehouseBuilding);
Warehouse.destroyedWarehouses.values().removeIf(destroyed -> destroyed.city == city);
city.warehouse = warehouse;
DbManager.WarehouseQueries.UPDATE_WAREHOUSE(warehouse);
@@ -1350,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,6 +13,7 @@ import engine.mbEnums;
import engine.net.Dispatch;
import engine.net.client.ClientConnection;
import engine.net.client.msg.ClientNetMsg;
import engine.net.client.msg.ErrorPopupMsg;
import engine.net.client.msg.RefineMsg;
import engine.net.client.msg.RefinerScreenMsg;
import engine.objects.*;
@@ -88,6 +89,9 @@ public class RefineMsgHandler extends AbstractClientMsgHandler {
public static boolean refineSkill(ClientConnection origin, PlayerCharacter pc, int token, RefineMsg msg) {
CharacterSkill skill = null;
ConcurrentHashMap<String, CharacterSkill> skills = pc.getSkills();
int refinedAmount;
int requiredAmount;
for (CharacterSkill sk : skills.values()) {
if (sk == null)
continue;
@@ -106,17 +110,16 @@ public class RefineMsgHandler extends AbstractClientMsgHandler {
//TODO verify if any skills have this as prereq
//TODO verify if any powers have this as a prereq
//get all players powers
// Dropping below a trained power's skill requirement would make calculatePowers delete the power and its trains
refinedAmount = Math.round(skill.getBaseAmountBeforeMods() + skill.calculateAmountAfterTrains(skill.getNumTrains() - 1));
for (CharacterPower power : pc.getPowers().values()) {
ArrayList<PowerReq> reqs = PowerReq.getPowerReqsForRune(power.getPowerID());
for (PowerReq req : reqs) {
ConcurrentHashMap<String, CharacterSkill> playerSkills = pc.getSkills();
CharacterSkill playerSkill = playerSkills.get(token);
int currentSkillLevel = playerSkill.getTotalSkillPercet();
if (token == req.getToken() && req.getLevel() == currentSkillLevel) {
return false;
}
if (power.getTrains() < 1)
continue;
requiredAmount = getRequiredSkillAmount(pc, power.getPower().getToken(), token);
if (refinedAmount < requiredAmount) {
ErrorPopupMsg.sendErrorMsg(pc, "You must refine " + power.getPower().getName() + " to 0 before refining any more from this skill.");
return false;
}
}
@@ -124,6 +127,36 @@ public class RefineMsgHandler extends AbstractClientMsgHandler {
return skill.refine(pc);
}
private static int getRequiredSkillAmount(PlayerCharacter pc, int powerToken, int skillToken) {
int requiredAmount = 0;
if (pc.race != null)
requiredAmount = Math.max(requiredAmount, getRequiredSkillAmount(pc.race.getPowersGranted(), powerToken, skillToken));
if (pc.baseClass != null)
requiredAmount = Math.max(requiredAmount, getRequiredSkillAmount(pc.baseClass.getPowersGranted(), powerToken, skillToken));
if (pc.promotionClass != null)
requiredAmount = Math.max(requiredAmount, getRequiredSkillAmount(pc.promotionClass.getPowersGranted(), powerToken, skillToken));
if (pc.getRunes() != null)
for (CharacterRune rune : pc.getRunes())
requiredAmount = Math.max(requiredAmount, getRequiredSkillAmount(rune.getPowersGranted(), powerToken, skillToken));
return requiredAmount;
}
private static int getRequiredSkillAmount(ArrayList<PowerReq> powersGranted, int powerToken, int skillToken) {
Byte requiredAmount;
for (PowerReq powerReq : powersGranted) {
if (powerReq.getToken() != powerToken)
continue;
requiredAmount = powerReq.getSkillReqs().get(skillToken);
if (requiredAmount != null)
return requiredAmount;
}
return 0;
}
public static boolean refinePower(ClientConnection origin, PlayerCharacter pc, int token, RefineMsg msg) {
CharacterPower power = null;
ConcurrentHashMap<Integer, CharacterPower> powers = pc.getPowers();
@@ -85,7 +85,7 @@ public class RepairMsgHandler extends AbstractClientMsgHandler {
//make sure item is damaged and not destroyed
short dur = (short) toRepair.combat_health_current;
short max = (short) toRepair.template.combat_health_full;
short max = (short) toRepair.template.getCombatHealthFull();
//account for durability modifications
@@ -68,7 +68,7 @@ public class TransferItemFromBankMsgHandler extends AbstractClientMsgHandler {
return true;
;
if (item.template.item_type.equals(mbEnums.ItemType.GOLD)) {
if (item.template.getItemType().equals(mbEnums.ItemType.GOLD)) {
if (!itemManager.moveGoldToInventory(item, msg.getNumItems()))
return true;
@@ -85,7 +85,7 @@ public class TransferItemFromBankMsgHandler extends AbstractClientMsgHandler {
// Not gold, process update here
if (!itemManager.hasRoomInventory(item.template.item_wt))
if (!itemManager.hasRoomInventory(item.template.getItemWt()))
return true;
if (itemManager.moveItemToInventory(item) == false)
@@ -65,7 +65,7 @@ public class TransferItemToBankMsgHandler extends AbstractClientMsgHandler {
return true;
if (item.containerType == mbEnums.ItemContainerType.INVENTORY && itemManager.isBankOpen())
if (item.template.item_type.equals(mbEnums.ItemType.GOLD)) {
if (item.template.getItemType().equals(mbEnums.ItemType.GOLD)) {
if (!itemManager.moveGoldToBank(item, msg.getNumItems()))
return true;
UpdateGoldMsg goldMes = new UpdateGoldMsg(player);
@@ -76,7 +76,7 @@ public class TransferItemToBankMsgHandler extends AbstractClientMsgHandler {
} else {
if (!itemManager.hasRoomBank(item.template.item_wt))
if (!itemManager.hasRoomBank(item.template.getItemWt()))
return true;
if (!itemManager.moveItemToBank(item))
@@ -86,7 +86,7 @@ public class VendorBuyMsgHandler extends AbstractClientMsgHandler {
if (me.objectUUID == vendorBuyMsg.getItemID()) {
//test room available for item
if (!itemMan.hasRoomInventory(me.template.item_wt))
if (!itemMan.hasRoomInventory(me.template.getItemWt()))
return true;
int cost = me.getValue();
@@ -148,10 +148,10 @@ public class VendorBuyMsgHandler extends AbstractClientMsgHandler {
//test room available for item
if (!itemMan.hasRoomInventory(vendorItem.template.item_wt))
if (!itemMan.hasRoomInventory(vendorItem.template.getItemWt()))
return true;
int cost = vendorItem.template.item_value;
int cost = vendorItem.template.getItemValue();
if (vendorItem.flags.contains(mbEnums.ItemFlags.Identified) || vendorItem.isCustomValue())
cost = vendorItem.magicValue;
@@ -94,7 +94,7 @@ public class VendorSellMsgHandler extends AbstractClientMsgHandler {
return true;
if (npc.getParentZone() != null && npc.getParentZone().playerCityUUID != 0)
if (!npc.charItemManager.hasRoomInventory(sell.template.item_wt)) {
if (!npc.charItemManager.hasRoomInventory(sell.template.getItemWt())) {
ErrorPopupMsg.sendErrorPopup(player, 21);
return true;
}
@@ -104,7 +104,7 @@ public class VendorSellMsgHandler extends AbstractClientMsgHandler {
//get goldItem cost to sell
cost = (int) (sell.template.item_value * (sell.combat_health_current / sell.template.combat_health_full));
cost = (int) (sell.template.getItemValue() * (sell.combat_health_current / sell.template.getCombatHealthFull()));
float bargain = player.getBargain();
float profit = npc.getBuyPercent(player) + bargain;
+73 -72
View File
@@ -22,10 +22,17 @@ import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class ApplyRuneMsg extends ClientNetMsg {
// Client ArcCharacter_AddRune (0x4695D0)
private static final int MAX_TALENTS = 8;
private int targetType;
private int targetID;
private int removeRuneBase;
@@ -78,67 +85,28 @@ public class ApplyRuneMsg extends ClientNetMsg {
return false;
}
//Check race is met
ConcurrentHashMap<Integer, Boolean> races = rb.getRace();
if (races.size() > 0) {
int raceID = playerCharacter.getRaceID();
boolean valid = false;
for (int validID : races.keySet()) {
if (validID == raceID) {
valid = true;
break;
}
}
if (!valid) {
return false;
}
}
//Check base class is met
ConcurrentHashMap<Integer, Boolean> baseClasses = rb.getBaseClass();
if (baseClasses.size() > 0) {
int baseClassID = playerCharacter.getBaseClassID();
boolean valid = false;
for (int validID : baseClasses.keySet()) {
if (validID == baseClassID) {
valid = true;
break;
}
}
if (!valid) {
return false;
}
}
//Check promotion class is met
ConcurrentHashMap<Integer, Boolean> promotionClasses = rb.getPromotionClass();
if (promotionClasses.size() > 0) {
int promotionClassID = playerCharacter.getPromotionClassID();
boolean valid = false;
for (int validID : promotionClasses.keySet()) {
if (validID == promotionClassID) {
valid = true;
break;
}
}
if (!valid) {
return false;
}
}
//Check disciplines are met
ArrayList<CharacterRune> runes = playerCharacter.getRunes();
ConcurrentHashMap<Integer, Boolean> disciplines = rb.getDiscipline();
if (disciplines.size() > 0) {
for (CharacterRune cr : runes) {
int runeBaseID = cr.getRuneBaseID();
for (Integer prohID : disciplines.keySet()) {
if (runeBaseID == prohID) {
return false; //Prohibited rune
}
}
}
}
HashSet<Integer> classIDs = new HashSet<>();
HashSet<Integer> heldDiscIDs = new HashSet<>();
// Client ArcCharacter_MeetsItemReqs (0x475F40)
if (!meetsListReq(rb.getRace().keySet(), rb.isRaceRestrict(), Collections.singleton(playerCharacter.getRaceID())))
return false;
// A listed base class covers its promotions: base or promotion listed is enough
classIDs.addAll(rb.getBaseClass().keySet());
classIDs.addAll(rb.getPromotionClass().keySet());
if (!meetsListReq(classIDs, rb.isClassRestrict(), Arrays.asList(playerCharacter.getBaseClassID(), playerCharacter.getPromotionClassID())))
return false;
// Only the new rune's list is checked, not the held discs' lists
for (CharacterRune cr : runes)
if (isDiscipline(cr.getRuneBaseID()))
heldDiscIDs.add(cr.getRuneBaseID());
if (!meetsListReq(rb.getDiscipline().keySet(), rb.isDiscRestrict(), heldDiscIDs))
return false;
int discCount = 0;
for (CharacterRune cr : runes) {
@@ -158,6 +126,32 @@ public class ApplyRuneMsg extends ClientNetMsg {
return false;
}
// Client 0x4768D0 refuses a talent unless its rank beats the held one with the same sub type (rank 0 included);
// AddRune (0x4695D0) then replaces the held talent
CharacterRune replacedRune = null;
if (rb.getRuneType() == mbEnums.RuneType.TALENT) {
int talentCount = 0;
for (CharacterRune cr : runes) {
RuneBase heldRuneBase = cr.getRuneBase();
if (heldRuneBase == null || heldRuneBase.getRuneType() != mbEnums.RuneType.TALENT)
continue;
talentCount++;
if (rb.getSubTypeToken() != 0 && heldRuneBase.getSubTypeToken() == rb.getSubTypeToken())
replacedRune = cr;
}
if (replacedRune != null) {
if (rb.getRank() <= replacedRune.getRuneBase().getRank())
return false;
} else if (talentCount >= MAX_TALENTS)
return false;
}
int strTotal = 0;
int dexTotal = 0;
int conTotal = 0;
@@ -227,11 +221,6 @@ public class ApplyRuneMsg extends ClientNetMsg {
}
}
//Check if max number runes already reached
if (runes.size() > 12) {
return false;
}
//if discipline, check number applied
if (isDiscipline(runeID)) {
if (playerCharacter.getLevel() < 70) {
@@ -259,13 +248,10 @@ public class ApplyRuneMsg extends ClientNetMsg {
return false;
}
//remove any overridden runes from player
ArrayList<Integer> overwrite = rb.getOverwrite();
//remove the replaced talent from player
CharacterRune toRemove = null;
if (overwrite.size() > 0) {
for (int overwriteID : overwrite) {
toRemove = playerCharacter.removeRune(overwriteID);
}
if (replacedRune != null) {
toRemove = playerCharacter.removeRune(replacedRune.getRuneBaseID());
}
//add rune to player
@@ -331,6 +317,21 @@ public class ApplyRuneMsg extends ClientNetMsg {
return runeID > 3000 && runeID < 3050;
}
// Client ArcItemRestrict_Check (0x4BAC60). An empty list has no requirement; no rune has an empty whitelist.
private static boolean meetsListReq(Set<Integer> listedIDs, boolean restrict, Collection<Integer> heldIDs) {
boolean listed = false;
if (listedIDs.isEmpty())
return true;
for (int heldID : heldIDs)
if (listedIDs.contains(heldID))
listed = true;
return listed != restrict;
}
/**
* Serializes the subclass specific items to the supplied NetMsgWriter.
*/
Loaded 100 of 316 files, more files were not shown because too many files have changed in this diff. Show more