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>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-27 06:40:59 -04:00
1 parent 2078a5c344
commit bb76596a9a
11 files changed
+77 -94

No files matched your search

@@ -64,8 +64,8 @@ public class dbBuildingHandler extends dbHandlerBase {
preparedStatement.setNull(13, java.sql.Types.DATE);
preparedStatement.setInt(14, building.getBlueprintUUID());
preparedStatement.setFloat(15, building.getw());
preparedStatement.setFloat(16, building.getRot().y);
preparedStatement.setFloat(15, building.getRotation().w);
preparedStatement.setFloat(16, building.getRotation().y);
ResultSet rs = preparedStatement.executeQuery();
+4 -9
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 = "";
@@ -93,7 +91,7 @@ public class AddBuildingCmd extends AbstractDevCmd {
Building building = Building.persistBuilding(Building.createBuilding(
zone, 0, buildingName, ID,
localLoc, 1.0f, 0, ProtectionState.PROTECTED, 0, rank,
null, blueprintUUID, w, rot.y));
null, blueprintUUID, rotation.w, rotation.y));
if (building == null) {
@@ -101,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,
+2 -2
View File
@@ -88,7 +88,7 @@ public class InfoCmd extends AbstractDevCmd {
output += newline;
output += StringUtils.addWS("Alt: " + targetLoc.y, 20);
output += newline;
output += "Rot: " + targetAWO.getRot().y;
output += "Rot: " + targetAWO.getRotation().y;
output += newline;
double radian = 0;
@@ -106,7 +106,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;
+21 -29
View File
@@ -14,7 +14,7 @@ import engine.devcmd.AbstractDevCmd;
import engine.gameManager.BuildingManager;
import engine.gameManager.ChatManager;
import engine.gameManager.DbManager;
import engine.math.Vector3f;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.objects.*;
@@ -63,14 +63,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, rot);
else if (target instanceof NPC)
rotateNPC(pc, (NPC) target, rotation, false);
rotateNPC(pc, (NPC) target, rot);
else if (target instanceof Mob)
rotateMob(pc, (Mob) target, rotation, false);
rotateMob(pc, (Mob) target, rot);
else
throwbackError(pc, "Target " + target.getObjectType()
+ " is not a valid object type");
@@ -141,15 +139,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, rot);
return;
}
@@ -160,7 +156,7 @@ public class RotateCmd extends AbstractDevCmd {
else
npc = getTargetAsNPC(pc);
if (npc != null) {
rotateNPC(pc, npc, rotation, false);
rotateNPC(pc, npc, rot);
return;
}
@@ -171,7 +167,7 @@ public class RotateCmd extends AbstractDevCmd {
else
mob = getTargetAsMob(pc);
if (mob != null) {
rotateMob(pc, mob, rotation, false);
rotateMob(pc, mob, rot);
return;
}
throwbackError(pc, "Nothing found to rotate.");
@@ -188,31 +184,29 @@ public class RotateCmd extends AbstractDevCmd {
return "' /rotate [objectID] rotation' || ' /rot [objectID] rotation'";
}
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 orig) {
building.setRotation(new Quaternion(0f, (float) Math.sin(Math.toRadians(orig) / 2), 0f,
(float) Math.abs(Math.cos(Math.toRadians(orig) / 2))));
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 rot) {
npc.setRotation(Quaternion.fromYaw((float) Math.acos(rot) * 2));
DbManager.NPCQueries.SET_PROPERTY(npc, "npc_rotation", rot);
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 rot) {
mob.setRotation(Quaternion.fromYaw((float) Math.acos(rot) * 2));
DbManager.MobQueries.SET_PROPERTY(mob, "mob_rotation", rot);
WorldGrid.updateObject(mob, pc);
//no rotation for mobs's in db currently
ChatManager.chatSayInfo(pc,
@@ -235,14 +229,12 @@ public class RotateCmd extends AbstractDevCmd {
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, rot);
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");
+4 -6
View File
@@ -11,7 +11,6 @@ package engine.gameManager;
import engine.InterestManagement.WorldGrid;
import engine.loot.WorkOrder;
import engine.math.Quaternion;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.net.Dispatch;
@@ -32,8 +31,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static engine.math.FastMath.acos;
public enum NPCManager {
NPC_MANAGER;
@@ -227,11 +224,12 @@ public enum NPCManager {
abstractCharacter.loc = new Vector3fImmutable(abstractCharacter.bindLoc);
// Rotate NPC rotation by the building's rotation
// Rotate NPC rotation by the building's rotation.
// Kept for wire parity: the product's sin θ/2 is read back as cos θ/2, so the yaw sent is not own + building.
Quaternion slotRotation = new Quaternion().fromAngles(0, acos(abstractCharacter.getRot().y) * 2, 0);
Quaternion slotRotation = new Quaternion().fromAngles(0, abstractCharacter.getRotation().getYaw(), 0);
slotRotation = slotRotation.mult(abstractCharacter.building.getBounds().getQuaternion());
abstractCharacter.setRot(new Vector3f(0, slotRotation.y, 0));
abstractCharacter.setRotation(Quaternion.fromYaw((float) Math.acos(slotRotation.y) * 2));
// Configure region and floor/level for this NPC
+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);
+13
View File
@@ -21,6 +21,7 @@ import engine.jobs.PersistentAoeJob;
import engine.jobs.TrackJob;
import engine.math.AtomicFloat;
import engine.math.Bounds;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
@@ -536,6 +537,18 @@ public abstract class AbstractCharacter extends AbstractWorldObject {
}
// The one yaw float LOADCHARACTER carries; a character in a building gets the building's yaw added.
public float getClientYaw() {
float yaw = this.getRotation().getYaw();
if (this.building != null)
if (this.building.getBounds() != null && this.building.getBounds().getQuaternion() != null)
yaw += this.building.getBounds().getQuaternion().angleY;
return yaw;
}
public static void teleport(AbstractCharacter abstractCharacter, final Vector3fImmutable targetLoc) {
abstractCharacter.locationLock.writeLock().lock();
try {
+8 -8
View File
@@ -18,7 +18,7 @@ import engine.job.JobScheduler;
import engine.jobs.NoTimeJob;
import engine.math.AtomicFloat;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums.DispatchChannel;
import engine.mbEnums.EffectSourceType;
@@ -56,7 +56,7 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
protected boolean movingUp = false;
private String name = "";
private byte tier = 0;
private Vector3f rot = new Vector3f(0.0f, 0.0f, 0.0f);
private Quaternion rotation = new Quaternion();
private int objectTypeMask = 0;
private Bounds bounds;
@@ -528,13 +528,13 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
}
public Vector3f getRot() {
return rot;
public Quaternion getRotation() {
return rotation;
}
public void setRot(Vector3f rotation) {
synchronized (this.rot) {
this.rot = rotation;
public void setRotation(Quaternion rotation) {
synchronized (this.rotation) {
this.rotation = rotation;
}
}
@@ -543,7 +543,7 @@ public abstract class AbstractWorldObject extends AbstractGameObject {
}
public void setTier(byte tier) {
synchronized (this.rot) {
synchronized (this.rotation) {
this.tier = tier;
}
}
+7 -17
View File
@@ -24,6 +24,7 @@ import engine.job.JobScheduler;
import engine.jobs.DoorCloseJob;
import engine.jobs.SiegeSpireWithdrawlJob;
import engine.math.Bounds;
import engine.math.Quaternion;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
@@ -85,7 +86,6 @@ public class Building extends AbstractWorldObject {
public ArrayList<Vector3fImmutable> patrolPoints;
public ProtectionState protectionState = ProtectionState.NONE;
protected Resists resists;
private float w = 1.0f;
private Vector3f meshScale = new Vector3f(1.0f, 1.0f, 1.0f);
private int doorState = 0;
private int ownerUUID = 0; //NPC or Character--check ownerIsNPC flag
@@ -128,8 +128,7 @@ public class Building extends AbstractWorldObject {
this.ownerUUID = rs.getInt("ownerUUID");
this.doorState = rs.getInt("doorState");
this.setHealth(rs.getInt("currentHP"));
this.w = rs.getFloat("w");
this.setRot(new Vector3f(0f, rs.getFloat("rotY"), 0f));
this.setRotation(new Quaternion(0f, rs.getFloat("rotY"), 0f, rs.getFloat("w")));
this.reverseKOS = rs.getByte("reverseKOS") == 1;
this.statLat = rs.getFloat("locationX");
this.statAlt = rs.getFloat("locationY");
@@ -207,8 +206,7 @@ public class Building extends AbstractWorldObject {
building._strongboxValue = currentGold;
building.maxGold = 15000000;
building.rank = rank;
building.w = w;
building.setRot(new Vector3f(0f, rotY, 0f));
building.setRotation(new Quaternion(0f, rotY, 0f, w));
building.parentBuildingID = 0;
building.floor = -1;
building.level = -1;
@@ -261,11 +259,11 @@ public class Building extends AbstractWorldObject {
writer.putFloat(building.getLoc().getZ());
}
writer.putFloat(building.w);
writer.putFloat(0f);
writer.putFloat(building.getRot().y);
writer.putFloat(building.getRotation().w);
writer.putFloat(building.getRotation().x);
writer.putFloat(building.getRotation().y);
writer.putFloat(building.getRotation().z);
writer.putFloat(0f);
writer.putFloat(building.meshScale.getX());
writer.putFloat(building.meshScale.getY());
writer.putFloat(building.meshScale.getZ());
@@ -752,14 +750,6 @@ public class Building extends AbstractWorldObject {
this.healthMax = maxHealth;
}
public final void setw(float value) {
this.w = value;
}
public final float getw() {
return this.w;
}
public final Vector3f getMeshScale() {
return this.meshScale;
}
+7 -8
View File
@@ -15,6 +15,8 @@ import engine.job.JobScheduler;
import engine.jobs.DeferredPowerJob;
import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
@@ -80,6 +82,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
// PERSIST.
public Mob() {
super();
this.setRotation(Quaternion.fromYaw(FastMath.PI));
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.bindLoc = Vector3fImmutable.ZERO;
@@ -99,6 +102,9 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
float statAlt;
float statLon;
// mob_rotation is not loaded: the old default (cos θ/2 = 0) faced every mob at yaw π.
this.setRotation(Quaternion.fromYaw(FastMath.PI));
try {
this.dbID = rs.getInt(1);
this.loadID = rs.getInt("mob_mobbaseID");
@@ -240,14 +246,7 @@ public class Mob extends AbstractIntelligenceAgent implements Delayed {
writer.putVector3f(mob.getLoc());
//Rotation
float radians = (float) Math.acos(mob.getRot().y) * 2;
if (mob.building != null)
if (mob.building.getBounds() != null && mob.building.getBounds().getQuaternion() != null)
radians += (mob.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
writer.putFloat(mob.getClientYaw());
//Inventory Stuff
+6 -10
View File
@@ -15,7 +15,8 @@ import engine.job.JobContainer;
import engine.job.JobScheduler;
import engine.jobs.UpgradeNPCJob;
import engine.math.Bounds;
import engine.math.Vector3f;
import engine.math.FastMath;
import engine.math.Quaternion;
import engine.math.Vector3fImmutable;
import engine.mbEnums;
import engine.mbEnums.*;
@@ -80,6 +81,7 @@ public class NPC extends AbstractCharacter {
public NPC() {
super();
this.setRotation(Quaternion.fromYaw(FastMath.PI));
this.dbID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
this.currentID = MBServerStatics.NO_DB_ROW_ASSIGNED_YET;
}
@@ -118,7 +120,8 @@ public class NPC extends AbstractCharacter {
this.buyPercent = .33f;
this.sellPercent = 1;
this.setRot(new Vector3f(0, rs.getFloat("npc_rotation"), 0));
// npc_rotation holds cos θ/2
this.setRotation(Quaternion.fromYaw((float) Math.acos(rs.getFloat("npc_rotation")) * 2));
this.statLat = rs.getFloat("npc_spawnX");
this.statAlt = rs.getFloat("npc_spawnY");
@@ -320,14 +323,7 @@ public class NPC extends AbstractCharacter {
else
writer.putVector3f(npc.getLoc());
//Rotation
float radians = (float) Math.acos(npc.getRot().y) * 2;
if (npc.building != null)
if (npc.building.getBounds() != null && npc.building.getBounds().getQuaternion() != null)
radians += (npc.building.getBounds().getQuaternion()).angleY;
writer.putFloat(radians);
writer.putFloat(npc.getClientYaw());
//Running Speed
writer.putInt(0);