Compare commits

..
Author SHA1 Message Date
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
20 changed files with 524 additions and 170 deletions

No files matched your search

+44 -30
View File
@@ -28,6 +28,7 @@ import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class dbBuildingHandler extends dbHandlerBase { public class dbBuildingHandler extends dbHandlerBase {
@@ -37,52 +38,45 @@ public class dbBuildingHandler extends dbHandlerBase {
this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName()); this.localObjectType = mbEnums.GameObjectType.valueOf(this.localClass.getSimpleName());
} }
public Building CREATE_BUILDING(int parentZoneID, int OwnerUUID, String name, int meshUUID, public int PERSIST(Building building) {
Vector3fImmutable location, float meshScale, int currentHP,
ProtectionState protectionState, int currentGold, int rank,
DateTime upgradeDate, int blueprintUUID, float w, float rotY) {
Building building = null; int objectUUID = 0;
try (Connection connection = DbManager.getConnection(); try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("CALL `building_CREATE`(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,? ,? ,?, ?);")) { PreparedStatement preparedStatement = connection.prepareStatement("CALL `building_CREATE`(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,? ,? ,?, ?);")) {
preparedStatement.setInt(1, parentZoneID); preparedStatement.setInt(1, building.parentZoneUUID);
preparedStatement.setInt(2, OwnerUUID); preparedStatement.setInt(2, building.getOwnerUUID());
preparedStatement.setString(3, name); preparedStatement.setString(3, building.getName());
preparedStatement.setInt(4, meshUUID); preparedStatement.setInt(4, building.meshUUID);
preparedStatement.setFloat(5, location.x); preparedStatement.setFloat(5, building.statLat);
preparedStatement.setFloat(6, location.y); preparedStatement.setFloat(6, building.statAlt);
preparedStatement.setFloat(7, location.z); preparedStatement.setFloat(7, building.statLon);
preparedStatement.setFloat(8, meshScale); preparedStatement.setFloat(8, building.getMeshScale().x);
preparedStatement.setInt(9, currentHP); preparedStatement.setInt(9, (int) building.getHealth());
preparedStatement.setString(10, protectionState.name()); preparedStatement.setString(10, building.getProtectionState().name());
preparedStatement.setInt(11, currentGold); preparedStatement.setInt(11, building.getStrongboxValue());
preparedStatement.setInt(12, rank); preparedStatement.setInt(12, building.getRank());
if (upgradeDate != null) if (building.upgradeDateTime != null)
preparedStatement.setTimestamp(13, new java.sql.Timestamp(upgradeDate.getMillis())); preparedStatement.setTimestamp(13, java.sql.Timestamp.valueOf(building.upgradeDateTime));
else else
preparedStatement.setNull(13, java.sql.Types.DATE); preparedStatement.setNull(13, java.sql.Types.DATE);
preparedStatement.setInt(14, blueprintUUID); preparedStatement.setInt(14, building.getBlueprintUUID());
preparedStatement.setFloat(15, w); preparedStatement.setFloat(15, building.getRotation().w);
preparedStatement.setFloat(16, rotY); preparedStatement.setFloat(16, building.getRotation().y);
ResultSet rs = preparedStatement.executeQuery(); ResultSet rs = preparedStatement.executeQuery();
if (rs.next()) { if (rs.next())
int objectUUID = (int) rs.getLong("UID"); objectUUID = (int) rs.getLong("UID");
if (objectUUID > 0)
building = GET_BUILDINGBYUUID(objectUUID);
}
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); Logger.error(e);
} }
return building; return objectUUID;
} }
public boolean DELETE_FROM_DATABASE(final Building b) { public boolean DELETE_FROM_DATABASE(final Building b) {
@@ -107,6 +101,26 @@ public class dbBuildingHandler extends dbHandlerBase {
return buildings; 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) { public Building GET_BUILDINGBYUUID(int uuid) {
if (uuid == 0) if (uuid == 0)
+6 -12
View File
@@ -14,7 +14,7 @@ import engine.devcmd.AbstractDevCmd;
import engine.gameManager.ChatManager; import engine.gameManager.ChatManager;
import engine.gameManager.DbManager; import engine.gameManager.DbManager;
import engine.gameManager.ZoneManager; import engine.gameManager.ZoneManager;
import engine.math.Vector3f; import engine.math.Quaternion;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums.ProtectionState; import engine.mbEnums.ProtectionState;
import engine.objects.*; import engine.objects.*;
@@ -54,8 +54,7 @@ public class AddBuildingCmd extends AbstractDevCmd {
"Invalid addBuilding Command. Invalid Building ID."); "Invalid addBuilding Command. Invalid Building ID.");
return; return;
} }
Vector3f rot = new Vector3f(0.0f, 0.0f, 0.0f); Quaternion rotation = new Quaternion();
float w = 1f;
Zone zone = ZoneManager.findSmallestZone(pc.getLoc()); Zone zone = ZoneManager.findSmallestZone(pc.getLoc());
if (zone == null) { if (zone == null) {
@@ -73,8 +72,7 @@ public class AddBuildingCmd extends AbstractDevCmd {
Building likeBuilding = DbManager.BuildingQueries.GET_BUILDING_BY_MESH(ID); Building likeBuilding = DbManager.BuildingQueries.GET_BUILDING_BY_MESH(ID);
if (likeBuilding != null) { if (likeBuilding != null) {
rot = likeBuilding.getRot(); rotation = likeBuilding.getRotation();
w = likeBuilding.getw();
} }
String buildingName = ""; String buildingName = "";
@@ -90,11 +88,10 @@ public class AddBuildingCmd extends AbstractDevCmd {
blueprintUUID = blueprint.getBlueprintUUID(); blueprintUUID = blueprint.getBlueprintUUID();
} }
Building building = DbManager.BuildingQueries. Building building = Building.persistBuilding(Building.createBuilding(
CREATE_BUILDING( zone, 0, buildingName, ID,
zone.getObjectUUID(), 0, buildingName, ID,
localLoc, 1.0f, 0, ProtectionState.PROTECTED, 0, rank, localLoc, 1.0f, 0, ProtectionState.PROTECTED, 0, rank,
null, blueprintUUID, w, rot.y); null, blueprintUUID, rotation.w, rotation.y));
if (building == null) { if (building == null) {
@@ -102,9 +99,6 @@ public class AddBuildingCmd extends AbstractDevCmd {
return; return;
} }
building.setRot(rot);
building.setw(w);
building.setObjectTypeMask(MBServerStatics.MASK_BUILDING); building.setObjectTypeMask(MBServerStatics.MASK_BUILDING);
WorldGrid.addObject(building, pc); WorldGrid.addObject(building, pc);
ChatManager.chatSayInfo(pc, ChatManager.chatSayInfo(pc,
+2 -2
View File
@@ -88,7 +88,7 @@ public class InfoCmd extends AbstractDevCmd {
output += newline; output += newline;
output += StringUtils.addWS("Alt: " + targetLoc.y, 20); output += StringUtils.addWS("Alt: " + targetLoc.y, 20);
output += newline; output += newline;
output += "Rot: " + targetAWO.getRot().y; output += "Rot: " + targetAWO.getRotation().y;
output += newline; output += newline;
double radian = 0; double radian = 0;
@@ -106,7 +106,7 @@ public class InfoCmd extends AbstractDevCmd {
case Building: case Building:
Building targetBuilding = (Building) target; Building targetBuilding = (Building) target;
output += StringUtils.addWS("Lac: " output += StringUtils.addWS("Lac: "
+ targetBuilding.getw(), 20); + targetBuilding.getRotation().w, 20);
output += "Blueprint : "; output += "Blueprint : ";
output += targetBuilding.getBlueprintUUID(); output += targetBuilding.getBlueprintUUID();
output += newline; output += newline;
+27 -37
View File
@@ -14,7 +14,8 @@ import engine.devcmd.AbstractDevCmd;
import engine.gameManager.BuildingManager; import engine.gameManager.BuildingManager;
import engine.gameManager.ChatManager; import engine.gameManager.ChatManager;
import engine.gameManager.DbManager; import engine.gameManager.DbManager;
import engine.math.Vector3f; import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.objects.*; import engine.objects.*;
@@ -63,14 +64,12 @@ public class RotateCmd extends AbstractDevCmd {
return; return;
} }
Vector3f rotation = new Vector3f(0f, rot, 0f);
if (target instanceof Building) if (target instanceof Building)
rotateBuilding(pc, (Building) target, rotation, rot, false); rotateBuilding(pc, (Building) target, (float) Math.toRadians(rot));
else if (target instanceof NPC) else if (target instanceof NPC)
rotateNPC(pc, (NPC) target, rotation, false); rotateNPC(pc, (NPC) target, (float) Math.toRadians(rot));
else if (target instanceof Mob) else if (target instanceof Mob)
rotateMob(pc, (Mob) target, rotation, false); rotateMob(pc, (Mob) target, (float) Math.toRadians(rot));
else else
throwbackError(pc, "Target " + target.getObjectType() throwbackError(pc, "Target " + target.getObjectType()
+ " is not a valid object type"); + " is not a valid object type");
@@ -141,15 +140,13 @@ public class RotateCmd extends AbstractDevCmd {
} }
} }
Vector3f rotation = new Vector3f(0f, rot, 0f);
Building b; Building b;
if (id != 0) if (id != 0)
b = BuildingManager.getBuilding(id); b = BuildingManager.getBuilding(id);
else else
b = getTargetAsBuilding(pc); b = getTargetAsBuilding(pc);
if (b != null) { if (b != null) {
rotateBuilding(pc, b, rotation, rot, false); rotateBuilding(pc, b, (float) Math.toRadians(rot));
return; return;
} }
@@ -160,7 +157,7 @@ public class RotateCmd extends AbstractDevCmd {
else else
npc = getTargetAsNPC(pc); npc = getTargetAsNPC(pc);
if (npc != null) { if (npc != null) {
rotateNPC(pc, npc, rotation, false); rotateNPC(pc, npc, (float) Math.toRadians(rot));
return; return;
} }
@@ -171,7 +168,7 @@ public class RotateCmd extends AbstractDevCmd {
else else
mob = getTargetAsMob(pc); mob = getTargetAsMob(pc);
if (mob != null) { if (mob != null) {
rotateMob(pc, mob, rotation, false); rotateMob(pc, mob, (float) Math.toRadians(rot));
return; return;
} }
throwbackError(pc, "Nothing found to rotate."); throwbackError(pc, "Nothing found to rotate.");
@@ -185,40 +182,40 @@ public class RotateCmd extends AbstractDevCmd {
@Override @Override
protected String _getUsageString() { 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) { private void rotateBuilding(PlayerCharacter pc, Building building, float yaw) {
if (!faceDirection) building.setRotation(Quaternion.fromYaw(yaw));
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)));
building.getBounds().setBounds(building); building.getBounds().setBounds(building);
WorldGrid.updateObject(building, pc); WorldGrid.updateObject(building, pc);
DbManager.BuildingQueries.SET_PROPERTY(building, "rotY", building.getRot().getY()); DbManager.BuildingQueries.SET_PROPERTY(building, "rotY", building.getRotation().y);
DbManager.BuildingQueries.SET_PROPERTY(building, "w", building.getw()); DbManager.BuildingQueries.SET_PROPERTY(building, "w", building.getRotation().w);
ChatManager.chatSayInfo(pc, ChatManager.chatSayInfo(pc,
"Building with ID " + building.getObjectUUID() + " rotated"); "Building with ID " + building.getObjectUUID() + " rotated");
} }
private void rotateNPC(PlayerCharacter pc, NPC npc, Vector3f rot, boolean faceDirection) { private void rotateNPC(PlayerCharacter pc, NPC npc, float yaw) {
npc.setRot(rot); npc.setRotation(characterRotation(yaw));
DbManager.NPCQueries.SET_PROPERTY(npc, "npc_rotation", rot.y); DbManager.NPCQueries.SET_PROPERTY(npc, "npc_rotation", npc.getRotation().w);
WorldGrid.updateObject(npc, pc); WorldGrid.updateObject(npc, pc);
//no rotation for npc's in db currently
ChatManager.chatSayInfo(pc, ChatManager.chatSayInfo(pc,
"NPC with ID " + npc.getObjectUUID() + " rotated"); "NPC with ID " + npc.getObjectUUID() + " rotated");
} }
private void rotateMob(PlayerCharacter pc, Mob mob, Vector3f rot, boolean faceDirection) { private void rotateMob(PlayerCharacter pc, Mob mob, float yaw) {
mob.setRot(rot); mob.setRotation(characterRotation(yaw));
DbManager.MobQueries.SET_PROPERTY(mob, "mob_rotation", rot.y); DbManager.MobQueries.SET_PROPERTY(mob, "mob_rotation", mob.getRotation().w);
WorldGrid.updateObject(mob, pc); WorldGrid.updateObject(mob, pc);
//no rotation for mobs's in db currently
ChatManager.chatSayInfo(pc, ChatManager.chatSayInfo(pc,
"Mob with ID " + mob.getObjectUUID() + " rotated"); "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) { private void rotateFace(PlayerCharacter pc, AbstractGameObject target) {
AbstractWorldObject awo = (AbstractWorldObject) target; AbstractWorldObject awo = (AbstractWorldObject) target;
if (awo == null) if (awo == null)
@@ -230,19 +227,12 @@ public class RotateCmd extends AbstractDevCmd {
float rotangle = faceDirection.getRotation(); 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) if (target instanceof Building)
rotateBuilding(pc, (Building) target, buildingrotation, rot, false); rotateBuilding(pc, (Building) target, rotangle);
else if (target instanceof NPC) else if (target instanceof NPC)
rotateNPC(pc, (NPC) target, rotation, true); rotateNPC(pc, (NPC) target, rotangle);
else if (target instanceof Mob) else if (target instanceof Mob)
rotateMob(pc, (Mob) target, rotation, true); rotateMob(pc, (Mob) target, rotangle);
else else
throwbackError(pc, "Target " + target.getObjectType() throwbackError(pc, "Target " + target.getObjectType()
+ " is not a valid object type"); + " is not a valid object type");
@@ -31,6 +31,7 @@ import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@@ -44,6 +45,7 @@ public enum BuildingManager {
public static HashMap<Integer, ConcurrentHashMap<Integer, BuildingFriends>> _buildingFriends = new HashMap<>(); public static HashMap<Integer, ConcurrentHashMap<Integer, BuildingFriends>> _buildingFriends = new HashMap<>();
public static HashMap<Integer, ConcurrentHashMap<Integer, Condemned>> _buildingCondemned = new HashMap<>(); public static HashMap<Integer, ConcurrentHashMap<Integer, Condemned>> _buildingCondemned = new HashMap<>();
public static HashMap<Integer, ArrayList<Vector3fImmutable>> _buildingPatrolPoints = new HashMap<>(); public static HashMap<Integer, ArrayList<Vector3fImmutable>> _buildingPatrolPoints = new HashMap<>();
public static HashSet<Integer> _npcBuildingOwners = new HashSet<>();
public static int getAvailableSlot(Building building) { public static int getAvailableSlot(Building building) {
+4 -6
View File
@@ -11,7 +11,6 @@ package engine.gameManager;
import engine.InterestManagement.WorldGrid; import engine.InterestManagement.WorldGrid;
import engine.loot.WorkOrder; import engine.loot.WorkOrder;
import engine.math.Quaternion; import engine.math.Quaternion;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums; import engine.mbEnums;
import engine.net.Dispatch; import engine.net.Dispatch;
@@ -32,8 +31,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static engine.math.FastMath.acos;
public enum NPCManager { public enum NPCManager {
NPC_MANAGER; NPC_MANAGER;
@@ -227,11 +224,12 @@ public enum NPCManager {
abstractCharacter.loc = new Vector3fImmutable(abstractCharacter.bindLoc); 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()); 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 // Configure region and floor/level for this NPC
+3 -3
View File
@@ -408,7 +408,7 @@ public class Bounds {
blueprint = building.getBlueprint(); 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 // Calculate Bounds for non-blueprint objects
@@ -437,7 +437,7 @@ public class Bounds {
// The rotation is reset after the new aabb is calculated. // 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 // Caclculate and set the new half halfExtents for the rotated bounding box
// and reset the rotation to 0 for this bounds. // 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.origin.set(building.getLoc().x, building.getLoc().z);
this.rotation = building.getRot().y; this.rotation = building.getRotation().y;
this.halfExtents.set(blueprint.getExtents()); this.halfExtents.set(blueprint.getExtents());
this.flipExtents = Bounds.calculateFlipExtents(this); this.flipExtents = Bounds.calculateFlipExtents(this);
+11
View File
@@ -1142,6 +1142,17 @@ public class Quaternion {
return opposite(this); 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 @Override
public Quaternion clone() { public Quaternion clone() {
try { 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);
}
}
@@ -1180,10 +1180,10 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
// if W return is negative, this is a -90 rotation not a 90? // if W return is negative, this is a -90 rotation not a 90?
newMesh = DbManager.BuildingQueries.CREATE_BUILDING( newMesh = Building.persistBuilding(Building.createBuilding(
currentZone.getObjectUUID(), playerCharacter.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0), currentZone, playerCharacter.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
localLoc, 1.0f, blueprint.getMaxHealth(0), ProtectionState.NONE, 0, 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 // Make sure we have a valid mesh
@@ -1303,10 +1303,10 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
DateTime completionDate = DateTime.now().plusHours(blueprint.getRankTime(1)); DateTime completionDate = DateTime.now().plusHours(blueprint.getRankTime(1));
newMesh = DbManager.BuildingQueries.CREATE_BUILDING( newMesh = Building.persistBuilding(Building.createBuilding(
currentZone.getObjectUUID(), player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0), currentZone, player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
localLoc, 1.0f, blueprint.getMaxHealth(0), ProtectionState.PROTECTED, 0, 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 // Make sure we have a valid mesh
if (newMesh == null) { if (newMesh == null) {
@@ -1342,10 +1342,10 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
City city = City.getCity(currentZone.playerCityUUID); City city = City.getCity(currentZone.playerCityUUID);
warehouseBuilding = DbManager.BuildingQueries.CREATE_BUILDING( warehouseBuilding = Building.persistBuilding(Building.createBuilding(
currentZone.getObjectUUID(), player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0), currentZone, player.getObjectUUID(), blueprint.getName(), blueprint.getMeshForRank(0),
localLoc, 1.0f, blueprint.getMaxHealth(0), ProtectionState.NONE, 0, 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) { if (warehouseBuilding == null) {
PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity"); PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity");
@@ -1363,7 +1363,6 @@ public class PlaceAssetMsgHandler extends AbstractClientMsgHandler {
MaintenanceManager.setMaintDateTime(warehouseBuilding, LocalDateTime.now().plusDays(7)); MaintenanceManager.setMaintDateTime(warehouseBuilding, LocalDateTime.now().plusDays(7));
warehouseBuilding.setLoc(warehouseBuilding.getLoc()); warehouseBuilding.setLoc(warehouseBuilding.getLoc());
InterestManager.setObjectDirty(warehouseBuilding); InterestManager.setObjectDirty(warehouseBuilding);
warehouseBuilding.runAfterLoad();
if (city == null) if (city == null)
return true; return true;
+13
View File
@@ -21,6 +21,7 @@ import engine.jobs.PersistentAoeJob;
import engine.jobs.TrackJob; import engine.jobs.TrackJob;
import engine.math.AtomicFloat; import engine.math.AtomicFloat;
import engine.math.Bounds; import engine.math.Bounds;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums; import engine.mbEnums;
import engine.mbEnums.*; import engine.mbEnums.*;
@@ -536,6 +537,18 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
} }
// The one yaw float LOADCHARACTER carries; a character in a building gets the building's yaw added.
public float getClientYaw() {
float yaw = this.getRotation().getYaw();
if (this.building != null)
if (this.building.getBounds() != null && this.building.getBounds().getQuaternion() != null)
yaw += this.building.getBounds().getQuaternion().angleY;
return yaw;
}
public static void teleport(AbstractCharacter abstractCharacter, final Vector3fImmutable targetLoc) { public static void teleport(AbstractCharacter abstractCharacter, final Vector3fImmutable targetLoc) {
abstractCharacter.locationLock.writeLock().lock(); abstractCharacter.locationLock.writeLock().lock();
try { try {
+8 -8
View File
@@ -18,7 +18,7 @@ import engine.job.JobScheduler;
import engine.jobs.NoTimeJob; import engine.jobs.NoTimeJob;
import engine.math.AtomicFloat; import engine.math.AtomicFloat;
import engine.math.Bounds; import engine.math.Bounds;
import engine.math.Vector3f; import engine.math.Quaternion;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums.DispatchChannel; import engine.mbEnums.DispatchChannel;
import engine.mbEnums.EffectSourceType; import engine.mbEnums.EffectSourceType;
@@ -56,7 +56,7 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
protected boolean movingUp = false; protected boolean movingUp = false;
private String name = ""; private String name = "";
private byte tier = 0; private byte tier = 0;
private Vector3f rot = new Vector3f(0.0f, 0.0f, 0.0f); private Quaternion rotation = new Quaternion();
private int objectTypeMask = 0; private int objectTypeMask = 0;
private Bounds bounds; private Bounds bounds;
@@ -528,13 +528,13 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
} }
public Vector3f getRot() { public Quaternion getRotation() {
return rot; return rotation;
} }
public void setRot(Vector3f rotation) { public void setRotation(Quaternion rotation) {
synchronized (this.rot) { synchronized (this.rotation) {
this.rot = rotation; this.rotation = rotation;
} }
} }
@@ -543,7 +543,7 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
} }
public void setTier(byte tier) { public void setTier(byte tier) {
synchronized (this.rot) { synchronized (this.rotation) {
this.tier = tier; this.tier = tier;
} }
} }
+3 -3
View File
@@ -210,10 +210,10 @@ public final class Bane {
return false; return false;
} }
Building stone = DbManager.BuildingQueries.CREATE_BUILDING( Building stone = Building.persistBuilding(Building.createBuilding(
cityZone.getObjectUUID(), player.getObjectUUID(), blueprint.getName(), blueprint.getBlueprintUUID(), cityZone, player.getObjectUUID(), blueprint.getName(), blueprint.getBlueprintUUID(),
localLocation, 1.0f, blueprint.getMaxHealth(rank), ProtectionState.PROTECTED, 0, rank, localLocation, 1.0f, blueprint.getMaxHealth(rank), ProtectionState.PROTECTED, 0, rank,
null, blueprint.getBlueprintUUID(), 1, 0.0f); null, blueprint.getBlueprintUUID(), 1, 0.0f));
if (stone == null) { if (stone == null) {
PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity"); PlaceAssetMsg.sendPlaceAssetError(player.getClientConnection(), 1, "A Serious error has occurred. Please post details for to ensure transaction integrity");
+71 -23
View File
@@ -24,6 +24,7 @@ import engine.job.JobScheduler;
import engine.jobs.DoorCloseJob; import engine.jobs.DoorCloseJob;
import engine.jobs.SiegeSpireWithdrawlJob; import engine.jobs.SiegeSpireWithdrawlJob;
import engine.math.Bounds; import engine.math.Bounds;
import engine.math.Quaternion;
import engine.math.Vector3f; import engine.math.Vector3f;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums; import engine.mbEnums;
@@ -32,10 +33,12 @@ import engine.net.ByteBufferWriter;
import engine.net.client.msg.ApplyBuildingEffectMsg; import engine.net.client.msg.ApplyBuildingEffectMsg;
import engine.net.client.msg.UpdateObjectMsg; import engine.net.client.msg.UpdateObjectMsg;
import engine.server.MBServerStatics; import engine.server.MBServerStatics;
import org.joda.time.DateTime;
import org.pmw.tinylog.Logger; import org.pmw.tinylog.Logger;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
@@ -83,7 +86,6 @@ public class Building extends AbstractWorldObject {
public ArrayList<Vector3fImmutable> patrolPoints; public ArrayList<Vector3fImmutable> patrolPoints;
public ProtectionState protectionState = ProtectionState.NONE; public ProtectionState protectionState = ProtectionState.NONE;
protected Resists resists; protected Resists resists;
private float w = 1.0f;
private Vector3f meshScale = new Vector3f(1.0f, 1.0f, 1.0f); private Vector3f meshScale = new Vector3f(1.0f, 1.0f, 1.0f);
private int doorState = 0; private int doorState = 0;
private int ownerUUID = 0; //NPC or Character--check ownerIsNPC flag private int ownerUUID = 0; //NPC or Character--check ownerIsNPC flag
@@ -99,6 +101,14 @@ public class Building extends AbstractWorldObject {
private ConcurrentHashMap<Integer, Condemned> condemned; private ConcurrentHashMap<Integer, Condemned> condemned;
private ArrayList<Building> children = null; private ArrayList<Building> children = null;
/**
* No Id Constructor
*/
public Building() {
super();
}
/** /**
* ResultSet Constructor * ResultSet Constructor
*/ */
@@ -118,8 +128,7 @@ public class Building extends AbstractWorldObject {
this.ownerUUID = rs.getInt("ownerUUID"); this.ownerUUID = rs.getInt("ownerUUID");
this.doorState = rs.getInt("doorState"); this.doorState = rs.getInt("doorState");
this.setHealth(rs.getInt("currentHP")); this.setHealth(rs.getInt("currentHP"));
this.w = rs.getFloat("w"); this.setRotation(new Quaternion(0f, rs.getFloat("rotY"), 0f, rs.getFloat("w")));
this.setRot(new Vector3f(0f, rs.getFloat("rotY"), 0f));
this.reverseKOS = rs.getByte("reverseKOS") == 1; this.reverseKOS = rs.getByte("reverseKOS") == 1;
this.statLat = rs.getFloat("locationX"); this.statLat = rs.getFloat("locationX");
this.statAlt = rs.getFloat("locationY"); this.statAlt = rs.getFloat("locationY");
@@ -173,6 +182,58 @@ public class Building extends AbstractWorldObject {
} }
} }
// Defaults mirror the obj_building column defaults so a new building matches a reloaded one.
public static Building createBuilding(Zone parentZone, int ownerUUID, String name, int meshUUID,
Vector3fImmutable localLoc, float scale, int currentHP,
ProtectionState protectionState, int currentGold, int rank,
DateTime upgradeDate, int blueprintUUID, float w, float rotY) {
Building building = new Building();
building.setObjectTypeMask(MBServerStatics.MASK_BUILDING);
building.gridObjectType = GridObjectType.STATIC;
building.parentZoneUUID = parentZone.getObjectUUID();
building.ownerUUID = ownerUUID;
building.name = name;
building.meshUUID = meshUUID;
building.blueprintUUID = blueprintUUID;
building.statLat = localLoc.x;
building.statAlt = localLoc.y;
building.statLon = localLoc.z;
building.meshScale = new Vector3f(scale, scale, scale);
building.setHealth(currentHP);
building.protectionState = protectionState;
building._strongboxValue = currentGold;
building.maxGold = 15000000;
building.rank = rank;
building.setRotation(new Quaternion(0f, rotY, 0f, w));
building.parentBuildingID = 0;
building.floor = -1;
building.level = -1;
if (upgradeDate != null)
building.upgradeDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(upgradeDate.getMillis()), ZoneId.systemDefault());
return building;
}
// Writes a new player building once and loads it into the simulation; returns null if the insert failed.
public static Building persistBuilding(Building building) {
int objectUUID;
objectUUID = DbManager.BuildingQueries.PERSIST(building);
if (objectUUID <= 0)
return null;
building.setObjectUUID(objectUUID);
DbManager.addToCache(building);
building.runAfterLoad();
return building;
}
/* /*
* Getters * Getters
*/ */
@@ -198,11 +259,11 @@ public class Building extends AbstractWorldObject {
writer.putFloat(building.getLoc().getZ()); writer.putFloat(building.getLoc().getZ());
} }
writer.putFloat(building.w); writer.putFloat(building.getRotation().w);
writer.putFloat(0f); writer.putFloat(building.getRotation().x);
writer.putFloat(building.getRot().y); writer.putFloat(building.getRotation().y);
writer.putFloat(building.getRotation().z);
writer.putFloat(0f);
writer.putFloat(building.meshScale.getX()); writer.putFloat(building.meshScale.getX());
writer.putFloat(building.meshScale.getY()); writer.putFloat(building.meshScale.getY());
writer.putFloat(building.meshScale.getZ()); writer.putFloat(building.meshScale.getZ());
@@ -689,14 +750,6 @@ public class Building extends AbstractWorldObject {
this.healthMax = maxHealth; this.healthMax = maxHealth;
} }
public final void setw(float value) {
this.w = value;
}
public final float getw() {
return this.w;
}
public final Vector3f getMeshScale() { public final Vector3f getMeshScale() {
return this.meshScale; return this.meshScale;
} }
@@ -964,15 +1017,10 @@ public class Building extends AbstractWorldObject {
} }
} }
// Submit upgrade job if building is currently set to rank. // City load reads ownerIsNPC before NPCs are cached, so boot uses the owner set read ahead of buildings.
try { this.ownerIsNPC = BuildingManager._npcBuildingOwners.contains(this.ownerUUID)
DbObjectType objectType = DbManager.BuildingQueries.GET_UID_ENUM(this.ownerUUID); || DbManager.inCache(GameObjectType.NPC, this.ownerUUID);
this.ownerIsNPC = (objectType == DbObjectType.NPC);
} catch (Exception e) {
this.ownerIsNPC = false;
Logger.error("Failed to find Object Type for owner " + this.ownerUUID + " Location " + this.getLoc().toString());
}
// Reference friend and condemn lists from BuildingManager // Reference friend and condemn lists from BuildingManager
+7 -8
View File
@@ -15,6 +15,8 @@ import engine.job.JobScheduler;
import engine.jobs.DeferredPowerJob; import engine.jobs.DeferredPowerJob;
import engine.jobs.UpgradeNPCJob; import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds; import engine.math.Bounds;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums; import engine.mbEnums;
import engine.mbEnums.*; import engine.mbEnums.*;
@@ -80,6 +82,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
// PERSIST. // PERSIST.
public Mob() { public Mob() {
super(); super();
this.setRotation(Quaternion.fromYaw(FastMath.PI));
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET; this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET; this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.bindLoc = Vector3fImmutable.ZERO; this.bindLoc = Vector3fImmutable.ZERO;
@@ -99,6 +102,9 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
float statAlt; float statAlt;
float statLon; float statLon;
// mob_rotation is not loaded: the old default (cos θ/2 = 0) faced every mob at yaw π.
this.setRotation(Quaternion.fromYaw(FastMath.PI));
try { try {
this.dbID = rs.getInt(1); this.dbID = rs.getInt(1);
this.loadID = rs.getInt("mob_mobbaseID"); this.loadID = rs.getInt("mob_mobbaseID");
@@ -240,14 +246,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
writer.putVector3f(mob.getLoc()); writer.putVector3f(mob.getLoc());
//Rotation writer.putFloat(mob.getClientYaw());
float radians = (float) Math.acos(mob.getRot().y) * 2;
if (mob.building != null)
if (mob.building.getBounds() != null && mob.building.getBounds().getQuaternion() != null)
radians += (mob.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
//Inventory Stuff //Inventory Stuff
+6 -10
View File
@@ -15,7 +15,8 @@ import engine.job.JobContainer;
import engine.job.JobScheduler; import engine.job.JobScheduler;
import engine.jobs.UpgradeNPCJob; import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds; import engine.math.Bounds;
import engine.math.Vector3f; import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable; import engine.math.Vector3fImmutable;
import engine.mbEnums; import engine.mbEnums;
import engine.mbEnums.*; import engine.mbEnums.*;
@@ -80,6 +81,7 @@ public class NPC extends AbstractCharacter {
public NPC() { public NPC() {
super(); super();
this.setRotation(Quaternion.fromYaw(FastMath.PI));
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET; this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET; this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
} }
@@ -118,7 +120,8 @@ public class NPC extends AbstractCharacter {
this.buyPercent = .33f; this.buyPercent = .33f;
this.sellPercent = 1; this.sellPercent = 1;
this.setRot(new Vector3f(0, rs.getFloat("npc_rotation"), 0)); // npc_rotation holds cos θ/2
this.setRotation(Quaternion.fromYaw((float) Math.acos(rs.getFloat("npc_rotation")) * 2));
this.statLat = rs.getFloat("npc_spawnX"); this.statLat = rs.getFloat("npc_spawnX");
this.statAlt = rs.getFloat("npc_spawnY"); this.statAlt = rs.getFloat("npc_spawnY");
@@ -320,14 +323,7 @@ public class NPC extends AbstractCharacter {
else else
writer.putVector3f(npc.getLoc()); writer.putVector3f(npc.getLoc());
//Rotation writer.putFloat(npc.getClientYaw());
float radians = (float) Math.acos(npc.getRot().y) * 2;
if (npc.building != null)
if (npc.building.getBounds() != null && npc.building.getBounds().getQuaternion() != null)
radians += (npc.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
//Running Speed //Running Speed
writer.putInt(0); writer.putInt(0);
@@ -135,12 +135,12 @@ public class PowerActionParser {
break; break;
case SetItemFlag: case SetItemFlag:
powerAction.item_flag = header.enumToken(mbEnums.ItemFlags.class, 2); powerAction.item_flag = header.enumToken(mbEnums.ItemFlags.class, 2);
powerAction.item_flag_value = header.optionalBoolToken(3); powerAction.item_flag_value = header.optionalIntToken(3) != 0;
break; break;
case Track: case Track:
powerAction.effect_ids.add(header.token(2)); powerAction.effect_ids.add(header.token(2));
powerAction.track_player = header.boolToken(3); powerAction.track_player = header.intToken(3) != 0;
powerAction.track_corpse = header.boolToken(4); powerAction.track_corpse = header.intToken(4) != 0;
powerAction.track_filter = header.token(5); powerAction.track_filter = header.token(5);
powerAction.max_track = header.intToken(7); powerAction.max_track = header.intToken(7);
break; break;
@@ -175,7 +175,7 @@ public class PowerActionParser {
powerAction.transfer_amount_curve = WpakParserUtil.curveToken(header, 5); powerAction.transfer_amount_curve = WpakParserUtil.curveToken(header, 5);
powerAction.transfer_efficiency = header.floatToken(6); powerAction.transfer_efficiency = header.floatToken(6);
powerAction.transfer_efficiency_curve = WpakParserUtil.curveToken(header, 7); powerAction.transfer_efficiency_curve = WpakParserUtil.curveToken(header, 7);
powerAction.transfer_target_to_caster = header.boolToken(8); powerAction.transfer_target_to_caster = header.intToken(8) != 0;
powerAction.isaggressive = header.optionalIntToken(9) != 0; powerAction.isaggressive = header.optionalIntToken(9) != 0;
} }
@@ -186,13 +186,13 @@ public class PowerActionParser {
switch (line.token(0)) { switch (line.token(0)) {
case "ISAGGRESSIVE=": case "ISAGGRESSIVE=":
powerAction.isaggressive = line.optionalBoolToken(1); powerAction.isaggressive = line.optionalExactTrueToken(1);
break; break;
case "BLADETRAILS=": case "BLADETRAILS=":
powerAction.bladetrails = line.optionalBoolToken(1); powerAction.bladetrails = line.optionalExactTrueToken(1);
break; break;
case "ISRESISTABLE=": case "ISRESISTABLE=":
powerAction.isresistable = line.optionalBoolToken(1); powerAction.isresistable = line.optionalExactTrueToken(1);
break; break;
case "APPLYEFFECTSELF=": case "APPLYEFFECTSELF=":
powerAction.applyeffectself = line.token(1); powerAction.applyeffectself = line.token(1);
@@ -225,10 +225,10 @@ public class PowerActionParser {
powerAction.femalebodyparts.addAll(line.intTokensFrom(1)); powerAction.femalebodyparts.addAll(line.intTokensFrom(1));
break; break;
case "SHOULDSHOWWEAPONS=": case "SHOULDSHOWWEAPONS=":
powerAction.shouldshowweapons = line.optionalBoolToken(1); powerAction.shouldshowweapons = line.optionalExactTrueToken(1);
break; break;
case "SHOULDSHOWARMOR=": case "SHOULDSHOWARMOR=":
powerAction.shouldshowarmor = line.optionalBoolToken(1); powerAction.shouldshowarmor = line.optionalExactTrueToken(1);
break; break;
case "ATTACKANIMS=": case "ATTACKANIMS=":
powerAction.attackanims.addAll(line.intTokensFrom(1)); powerAction.attackanims.addAll(line.intTokensFrom(1));
+30 -9
View File
@@ -20,8 +20,8 @@ import java.util.regex.Pattern;
public class WpakLine { public class WpakLine {
// The client reads numbers like C atoi/atof: the leading number counts, the rest is dropped // The client reads numbers with strtol/strtod (Core.dll String::ToInt/ToFloat): the leading
// (Powers.cfg has "2.9.0" for a float and "10.0" for an int) // number counts, the rest is dropped, so "2.9.0" is 2.9 and "10.0" as an int is 10
private static final Pattern intPrefix = Pattern.compile("[-+]?\\d+"); private static final Pattern intPrefix = Pattern.compile("[-+]?\\d+");
private static final Pattern floatPrefix = Pattern.compile("[-+]?(\\d+\\.?\\d*|\\.\\d+)"); private static final Pattern floatPrefix = Pattern.compile("[-+]?(\\d+\\.?\\d*|\\.\\d+)");
@@ -107,23 +107,44 @@ public class WpakLine {
return intToken(index); return intToken(index);
} }
// TRUE/FALSE, or a number where non-zero is true // Core.dll String::ToBool with the false default every wpak loader passes: a leading '0' is
// false, '1'..'9' is true, then TRUE/FALSE in any case; anything else reads as false
public boolean boolToken(int index) { public boolean boolToken(int index) {
String value = token(index); String value = token(index);
char first = value.isEmpty() ? 0 : value.charAt(0);
if (first == '0')
return false;
if (first >= '1' && first <= '9')
return true;
if (value.equalsIgnoreCase("TRUE")) if (value.equalsIgnoreCase("TRUE"))
return true; return true;
if (value.equalsIgnoreCase("FALSE")) if (!value.equalsIgnoreCase("FALSE"))
Logger.warn(location() + ": token " + index + " '" + value + "' read as false, as the client does");
return false;
}
// Some PowerActions keys compare the token to "TRUE" exactly (String_EqualsCStr 0x546450), so "1" is false
public boolean optionalExactTrueToken(int index) {
String value;
if (!has(index))
return false; return false;
try { value = token(index);
return Integer.parseInt(value) != 0;
} catch (NumberFormatException e) { if (!value.equals("TRUE") && !value.equals("FALSE"))
throw error("token " + index + " '" + value + "' is not a boolean"); Logger.warn(location() + ": token " + index + " '" + value + "' read as false, as the client does");
}
return value.equals("TRUE");
} }
public boolean optionalBoolToken(int index) { public boolean optionalBoolToken(int index) {
+11
View File
@@ -62,6 +62,7 @@ import engine.powers.RunePowerEntry;
import engine.powers.RuneSkillAdjustEntry; import engine.powers.RuneSkillAdjustEntry;
import engine.server.MBServerStatics; import engine.server.MBServerStatics;
import engine.util.MapLoader; import engine.util.MapLoader;
import engine.util.RotationParityCheck;
import engine.util.ThreadUtils; import engine.util.ThreadUtils;
import engine.workthreads.DisconnectTrashTask; import engine.workthreads.DisconnectTrashTask;
import engine.workthreads.HourlyJobThread; import engine.workthreads.HourlyJobThread;
@@ -529,16 +530,26 @@ public class WorldServer {
Logger.info("Loading all Zones"); Logger.info("Loading all Zones");
DbManager.ZoneQueries.GET_ALL_ZONES(); DbManager.ZoneQueries.GET_ALL_ZONES();
Logger.info("Loading all Buildings"); Logger.info("Loading all Buildings");
BuildingManager._npcBuildingOwners = DbManager.BuildingQueries.GET_NPC_BUILDING_OWNERS();
DbManager.BuildingQueries.GET_ALL_BUILDINGS(); DbManager.BuildingQueries.GET_ALL_BUILDINGS();
Logger.info("Loading all Cities"); Logger.info("Loading all Cities");
DbManager.CityQueries.GET_ALL_CITIES(); DbManager.CityQueries.GET_ALL_CITIES();
Logger.info("Loading all NPCs"); Logger.info("Loading all NPCs");
DbManager.NPCQueries.GET_ALL_NPCS(); DbManager.NPCQueries.GET_ALL_NPCS();
// NPCs are cached now; Building.runAfterLoad falls back to the NPC cache.
BuildingManager._npcBuildingOwners.clear();
Logger.info("Loading all Mobiles"); Logger.info("Loading all Mobiles");
DbManager.MobQueries.GET_ALL_MOBS(); DbManager.MobQueries.GET_ALL_MOBS();
Logger.info("time to load World Objects: " + (System.currentTimeMillis() - start) + " ms"); Logger.info("time to load World Objects: " + (System.currentTimeMillis() - start) + " ms");
if (ParityCheck.isEnabled()) {
Logger.info("Running rotation parity check");
RotationParityCheck.run();
}
// Configure realms for serialization // Configure realms for serialization
// Doing this after the world is loaded // Doing this after the world is loaded
+213
View File
@@ -0,0 +1,213 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2026
// https://www.magicbane.com
package engine.util;
import engine.gameManager.DbManager;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.mbEnums.GameObjectType;
import engine.mbEnums.MobBehaviourType;
import engine.objects.*;
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;
// Dev-only, World-Boot-Plan 0.4: replays the pre-quaternion rotation code on the DB values and compares its wire
// values with the loaded objects. Runs under MB_PARITY_CHECK; delete once a run is clean.
public class RotationParityCheck {
private static final float TOLERANCE = 1.0e-5f;
private static final int MAX_LOGGED = 20;
private final String label;
private int comparedCount;
private int differenceCount;
private float maxDifference;
private RotationParityCheck(String label) {
this.label = label;
}
public static void run() {
HashMap<Integer, float[]> buildingRows;
HashMap<Integer, Float> npcRows;
buildingRows = readBuildingRows();
npcRows = readNpcRows();
checkBuildings(buildingRows);
checkNPCs(npcRows);
checkMobs();
}
private static void checkBuildings(HashMap<Integer, float[]> buildingRows) {
RotationParityCheck check = new RotationParityCheck("Building");
Building building;
Quaternion rotation;
float[] row;
for (AbstractGameObject object : DbManager.getList(GameObjectType.Building)) {
building = (Building) object;
row = buildingRows.get(building.getObjectUUID());
if (row == null)
continue;
rotation = building.getRotation();
// The old serializer wrote (w, 0, rotY, 0).
check.compare(building.getObjectUUID(), "w", row[0], rotation.w);
check.compare(building.getObjectUUID(), "x", 0f, rotation.x);
check.compare(building.getObjectUUID(), "y", row[1], rotation.y);
check.compare(building.getObjectUUID(), "z", 0f, rotation.z);
}
check.report();
}
private static void checkNPCs(HashMap<Integer, Float> npcRows) {
RotationParityCheck check = new RotationParityCheck("NPC");
NPC npc;
Float halfYawCosine;
for (AbstractGameObject object : DbManager.getList(GameObjectType.NPC)) {
npc = (NPC) object;
halfYawCosine = npcRows.get(npc.getObjectUUID());
if (halfYawCosine == null)
continue;
check.compare(npc.getObjectUUID(), "yaw", legacyYaw(npc, halfYawCosine, npc.building != null), npc.getClientYaw());
}
check.report();
}
private static void checkMobs() {
RotationParityCheck check = new RotationParityCheck("Mob");
Mob mob;
boolean slotted;
for (AbstractGameObject object : DbManager.getList(GameObjectType.Mob)) {
mob = (Mob) object;
slotted = mob.building != null && (mob.contract != null || mob.behaviourType == MobBehaviourType.SiegeEngine);
// Mob never loaded mob_rotation, so the old Vector3f default of 0 was its cos θ/2.
check.compare(mob.getObjectUUID(), "yaw", legacyYaw(mob, 0f, slotted), mob.getClientYaw());
}
check.report();
}
// The old slot code, rot.y holder and serializer, from the commit before the quaternion switch.
private static float legacyYaw(AbstractCharacter character, float halfYawCosine, boolean slotted) {
Quaternion buildingRotation = null;
Quaternion slotRotation;
float yaw;
if (character.building != null && character.building.getBounds() != null)
buildingRotation = character.building.getBounds().getQuaternion();
if (slotted && buildingRotation != null) {
slotRotation = new Quaternion().fromAngles(0, FastMath.acos(halfYawCosine) * 2, 0);
slotRotation = slotRotation.mult(buildingRotation);
halfYawCosine = slotRotation.y;
}
yaw = (float) Math.acos(halfYawCosine) * 2;
if (buildingRotation != null)
yaw += buildingRotation.angleY;
return yaw;
}
private static HashMap<Integer, float[]> readBuildingRows() {
HashMap<Integer, float[]> rows = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `UID`, `w`, `rotY` FROM `obj_building`;")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
rows.put(rs.getInt("UID"), new float[]{rs.getFloat("w"), rs.getFloat("rotY")});
} catch (SQLException e) {
Logger.error(e);
}
return rows;
}
private static HashMap<Integer, Float> readNpcRows() {
HashMap<Integer, Float> rows = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT `UID`, `npc_rotation` FROM `obj_npc`;")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next())
rows.put(rs.getInt("UID"), rs.getFloat("npc_rotation"));
} catch (SQLException e) {
Logger.error(e);
}
return rows;
}
// Angles are compared modulo 2π: the client only feeds them to MakeRotateY.
private void compare(int id, String field, float legacyValue, float newValue) {
float difference;
comparedCount++;
if (Float.isNaN(legacyValue) && Float.isNaN(newValue))
return;
difference = Math.abs(newValue - legacyValue);
if (field.equals("yaw"))
difference = Math.abs((float) Math.IEEEremainder(newValue - legacyValue, FastMath.TWO_PI));
if (Float.isNaN(difference))
difference = Float.POSITIVE_INFINITY;
maxDifference = Math.max(maxDifference, difference);
if (difference <= TOLERANCE)
return;
differenceCount++;
if (differenceCount <= MAX_LOGGED)
Logger.warn("Rotation parity: " + label + "/" + id + " " + field + ": old " + legacyValue + ", new " + newValue);
}
private void report() {
Logger.info("Rotation parity: " + label + ": " + comparedCount + " compared, " + differenceCount
+ " differences over " + TOLERANCE + ", max difference " + maxDifference);
}
}