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>
This commit is contained in:
1 parent
bb76596a9a
commit
e288524ec0
2 files changed
+219
No files matched your search
@@ -62,6 +62,7 @@ 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;
|
||||
@@ -544,6 +545,11 @@ 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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user