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>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-27 06:50:32 -04:00
1 parent bbf978872b
commit 740719aeb2
2 files changed
-219

No files matched your search

-6
View File
@@ -62,7 +62,6 @@ import engine.powers.RunePowerEntry;
import engine.powers.RuneSkillAdjustEntry;
import engine.server.MBServerStatics;
import engine.util.MapLoader;
import engine.util.RotationParityCheck;
import engine.util.ThreadUtils;
import engine.workthreads.DisconnectTrashTask;
import engine.workthreads.HourlyJobThread;
@@ -545,11 +544,6 @@ public class WorldServer {
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
// Doing this after the world is loaded
-213
View File
@@ -1,213 +0,0 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// 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);
}
}