From b70febb678ed29e9b144318a8a22ef6717caceb3 Mon Sep 17 00:00:00 2001 From: austin Date: Fri, 30 May 2025 16:01:30 -0400 Subject: [PATCH] synchronized time-of-day --- docs/src/progress/renderertodo.md | 1 + .../electrosphere/client/ClientState.java | 8 +++ .../client/service/ClientTemporalService.java | 64 +++++++++++++++++++ .../client/sim/ClientSimulation.java | 10 +++ .../macro/temporal/MacroTemporalData.java | 64 +++++++++++++++++++ .../net/client/protocol/LoreProtocol.java | 15 +++-- .../net/parser/net/message/LoreMessage.java | 61 ++++++++++++++++++ .../parser/net/message/NetworkMessage.java | 3 + .../net/parser/net/message/TypeBytes.java | 1 + .../java/electrosphere/net/server/Server.java | 4 +- .../net/server/protocol/LoreProtocol.java | 2 + .../electrosphere/server/datacell/Realm.java | 31 ++++----- .../electrosphere/server/macro/MacroData.java | 14 ++++ .../server/simulation/MacroSimulation.java | 6 ++ .../temporal/TemporalSimulator.java | 37 +++++++++++ .../util/math/BasicMathUtils.java | 19 ++++++ src/net/lore.json | 7 ++ 17 files changed, 322 insertions(+), 25 deletions(-) create mode 100644 src/main/java/electrosphere/client/service/ClientTemporalService.java create mode 100644 src/main/java/electrosphere/data/macro/temporal/MacroTemporalData.java create mode 100644 src/main/java/electrosphere/server/simulation/temporal/TemporalSimulator.java create mode 100644 src/main/java/electrosphere/util/math/BasicMathUtils.java diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 68a07579..729402f0 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -2081,6 +2081,7 @@ Pathing construction for farm plots Bounding sphere work Don't allocate contact joints for geom-geom Remove potential collision engine footgun +Synchronized time-of-day between server and client diff --git a/src/main/java/electrosphere/client/ClientState.java b/src/main/java/electrosphere/client/ClientState.java index 680d5af3..b1bb29a6 100644 --- a/src/main/java/electrosphere/client/ClientState.java +++ b/src/main/java/electrosphere/client/ClientState.java @@ -10,6 +10,7 @@ import electrosphere.client.player.ClientPlayerData; import electrosphere.client.scene.ClientLevelEditorData; import electrosphere.client.scene.ClientSceneWrapper; import electrosphere.client.scene.ClientWorldData; +import electrosphere.client.service.ClientTemporalService; import electrosphere.client.sim.ClientSimulation; import electrosphere.client.terrain.cells.ClientDrawCellManager; import electrosphere.client.terrain.foliage.FoliageCellManager; @@ -17,6 +18,7 @@ import electrosphere.client.terrain.manager.ClientTerrainManager; import electrosphere.collision.CollisionEngine; import electrosphere.data.entity.common.CommonEntityType; import electrosphere.data.voxel.VoxelType; +import electrosphere.engine.Globals; import electrosphere.entity.Entity; import electrosphere.entity.scene.Scene; import electrosphere.net.client.ClientNetworking; @@ -178,11 +180,17 @@ public class ClientState { */ public int openInventoriesCount = 0; + // + //Services + // + public final ClientTemporalService clientTemporalService; + /** * Constructor */ public ClientState(){ this.clientSceneWrapper = new ClientSceneWrapper(this.clientScene, new CollisionEngine(), CollisionEngine.create(new ClientChemistryCollisionCallback()), new CollisionEngine()); + this.clientTemporalService = (ClientTemporalService)Globals.engineState.serviceManager.registerService(new ClientTemporalService()); } } diff --git a/src/main/java/electrosphere/client/service/ClientTemporalService.java b/src/main/java/electrosphere/client/service/ClientTemporalService.java new file mode 100644 index 00000000..cb423a0a --- /dev/null +++ b/src/main/java/electrosphere/client/service/ClientTemporalService.java @@ -0,0 +1,64 @@ +package electrosphere.client.service; + +import electrosphere.engine.signal.Signal.SignalType; +import electrosphere.server.simulation.temporal.TemporalSimulator; +import electrosphere.util.math.BasicMathUtils; + +import java.util.concurrent.locks.ReentrantLock; + +import electrosphere.data.macro.temporal.MacroTemporalData; +import electrosphere.engine.signal.SignalServiceImpl; + +/** + * Synchronizes and interpolates temporal data between server and client + */ +public class ClientTemporalService extends SignalServiceImpl { + + /** + * Lerp rate for synchronization + */ + private static final double LERP_RATE = 1.0 / (double)TemporalSimulator.TEMPORAL_SYNC_RATE; + + /** + * The client's stored temporal data + */ + private MacroTemporalData clientTemporalData = new MacroTemporalData(); + + /** + * The latest temporal data from the server + */ + private MacroTemporalData latestServerData = new MacroTemporalData(); + + /** + * Lock for thread-safeing the service + */ + private ReentrantLock lock = new ReentrantLock(); + + /** + * Constructor + */ + public ClientTemporalService() { + super("ClientTemporalService", new SignalType[]{ + }); + } + + /** + * Simulates the service + */ + public void simulate(){ + lock.lock(); + clientTemporalData.setTime((long)BasicMathUtils.lerp((double)clientTemporalData.getTime(), (double)latestServerData.getTime(), LERP_RATE)); + lock.unlock(); + } + + /** + * Sets the latest server data + * @param serverData The latest server data + */ + public void setLatestData(MacroTemporalData serverData){ + lock.lock(); + this.latestServerData = serverData; + lock.unlock(); + } + +} diff --git a/src/main/java/electrosphere/client/sim/ClientSimulation.java b/src/main/java/electrosphere/client/sim/ClientSimulation.java index 155e1a47..40702d9b 100644 --- a/src/main/java/electrosphere/client/sim/ClientSimulation.java +++ b/src/main/java/electrosphere/client/sim/ClientSimulation.java @@ -152,10 +152,20 @@ public class ClientSimulation { AttachUtils.clientUpdateAttachedEntityPositions(); ClientInteractionEngine.updateInteractionTargetLabel(); ToolbarPreviewWindow.checkVisibility(); + this.runServices(); // updateCellManager(); Globals.profiler.endCpuSample(); } + /** + * Runs the client services + */ + private void runServices(){ + Globals.profiler.beginCpuSample("ClientSimulation.runServices"); + Globals.clientState.clientTemporalService.simulate(); + Globals.profiler.endCpuSample(); + } + /** * Updates the skybox position to center on the player diff --git a/src/main/java/electrosphere/data/macro/temporal/MacroTemporalData.java b/src/main/java/electrosphere/data/macro/temporal/MacroTemporalData.java new file mode 100644 index 00000000..46cec298 --- /dev/null +++ b/src/main/java/electrosphere/data/macro/temporal/MacroTemporalData.java @@ -0,0 +1,64 @@ +package electrosphere.data.macro.temporal; + +/** + * Temporal data associated with the macro data (ie calendar date, world age, etc) + */ +public class MacroTemporalData { + + /** + * Amount of time per day + */ + public static final long TIME_PER_DAY = 60 * 60 * 30; + + /** + * The noon remainder amount + */ + public static final long TIME_NOON = TIME_PER_DAY / 2; + + /** + * The midnight remainder amount + */ + public static final long TIME_MIDNIGHT = 0; + + /** + * Total age of the macro data in years + */ + private long age; + + /** + * The time WITHIN THE CURRENT YEAR + */ + private long time; + + /** + * Increments the time of the temporal data by some amount + */ + public void increment(long amount){ + this.time = this.time + amount; + } + + /** + * Gets the time WITHIN THE CURRENT YEAR of the data + * @return The time + */ + public long getTime(){ + return this.time; + } + + /** + * Gets the age of the world in years + * @return The age of the world in years + */ + public long getAge(){ + return age; + } + + /** + * Sets the time of the temporal data + * @param time The time + */ + public void setTime(long time){ + this.time = time; + } + +} diff --git a/src/main/java/electrosphere/net/client/protocol/LoreProtocol.java b/src/main/java/electrosphere/net/client/protocol/LoreProtocol.java index 05932db6..e6ff9309 100644 --- a/src/main/java/electrosphere/net/client/protocol/LoreProtocol.java +++ b/src/main/java/electrosphere/net/client/protocol/LoreProtocol.java @@ -4,9 +4,11 @@ import java.util.List; import com.google.gson.Gson; +import electrosphere.data.macro.temporal.MacroTemporalData; import electrosphere.engine.Globals; import electrosphere.net.parser.net.message.LoreMessage; import electrosphere.net.template.ClientProtocolTemplate; +import electrosphere.util.SerializationUtils; public class LoreProtocol implements ClientProtocolTemplate { @@ -18,15 +20,20 @@ public class LoreProtocol implements ClientProtocolTemplate { @Override public void handleSyncMessage(LoreMessage message) { switch(message.getMessageSubtype()){ - case RESPONSERACES: + case RESPONSERACES: { //we get back the race list as a json array, deserialize, and push into type loader @SuppressWarnings("unchecked") List playableRaces = new Gson().fromJson(message.getdata(), List.class); Globals.gameConfigCurrent.getCreatureTypeLoader().loadPlayableRaces(playableRaces); - break; - case REQUESTRACES: + } break; + case TEMPORALUPDATE: { + MacroTemporalData temporalData = SerializationUtils.deserialize(message.getdata(), MacroTemporalData.class); + Globals.clientState.clientTemporalService.setLatestData(temporalData); + } break; + + case REQUESTRACES: { //silently ignore - break; + } break; } } diff --git a/src/main/java/electrosphere/net/parser/net/message/LoreMessage.java b/src/main/java/electrosphere/net/parser/net/message/LoreMessage.java index 21234820..7e60cba1 100644 --- a/src/main/java/electrosphere/net/parser/net/message/LoreMessage.java +++ b/src/main/java/electrosphere/net/parser/net/message/LoreMessage.java @@ -15,6 +15,7 @@ public class LoreMessage extends NetworkMessage { public enum LoreMessageType { REQUESTRACES, RESPONSERACES, + TEMPORALUPDATE, } /** @@ -107,6 +108,36 @@ public class LoreMessage extends NetworkMessage { return rVal; } + /** + * Parses a message of type TemporalUpdate + */ + public static LoreMessage parseTemporalUpdateMessage(ByteBuffer byteBuffer, MessagePool pool, Map> customParserMap){ + if(byteBuffer.remaining() < 4){ + return null; + } + int lenAccumulator = 0; + int datalen = byteBuffer.getInt(); + lenAccumulator = lenAccumulator + datalen; + if(byteBuffer.remaining() < 4 + lenAccumulator){ + return null; + } + LoreMessage rVal = (LoreMessage)pool.get(MessageType.LORE_MESSAGE); + rVal.messageType = LoreMessageType.TEMPORALUPDATE; + if(datalen > 0){ + rVal.setdata(ByteStreamUtils.popStringFromByteBuffer(byteBuffer, datalen)); + } + return rVal; + } + + /** + * Constructs a message of type TemporalUpdate + */ + public static LoreMessage constructTemporalUpdateMessage(String data){ + LoreMessage rVal = new LoreMessage(LoreMessageType.TEMPORALUPDATE); + rVal.setdata(data); + return rVal; + } + @Deprecated @Override void serialize(){ @@ -135,6 +166,21 @@ public class LoreMessage extends NetworkMessage { rawBytes[6+i] = stringBytes[i]; } break; + case TEMPORALUPDATE: + rawBytes = new byte[2+4+data.length()]; + //message header + rawBytes[0] = TypeBytes.MESSAGE_TYPE_LORE; + //entity messaage header + rawBytes[1] = TypeBytes.LORE_MESSAGE_TYPE_TEMPORALUPDATE; + intValues = ByteStreamUtils.serializeIntToBytes(data.length()); + for(int i = 0; i < 4; i++){ + rawBytes[2+i] = intValues[i]; + } + stringBytes = data.getBytes(); + for(int i = 0; i < data.length(); i++){ + rawBytes[6+i] = stringBytes[i]; + } + break; } serialized = true; } @@ -163,6 +209,21 @@ public class LoreMessage extends NetworkMessage { //Write variable length table in packet ByteStreamUtils.writeInt(stream, data.getBytes().length); + // + //Write body of packet + ByteStreamUtils.writeString(stream, data); + } break; + case TEMPORALUPDATE: { + + // + //message header + stream.write(TypeBytes.MESSAGE_TYPE_LORE); + stream.write(TypeBytes.LORE_MESSAGE_TYPE_TEMPORALUPDATE); + + // + //Write variable length table in packet + ByteStreamUtils.writeInt(stream, data.getBytes().length); + // //Write body of packet ByteStreamUtils.writeString(stream, data); diff --git a/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java b/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java index 859844fa..32bbf07d 100644 --- a/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java +++ b/src/main/java/electrosphere/net/parser/net/message/NetworkMessage.java @@ -126,6 +126,9 @@ public abstract class NetworkMessage { case TypeBytes.LORE_MESSAGE_TYPE_RESPONSERACES: rVal = LoreMessage.parseResponseRacesMessage(byteBuffer,pool,customParserMap); break; + case TypeBytes.LORE_MESSAGE_TYPE_TEMPORALUPDATE: + rVal = LoreMessage.parseTemporalUpdateMessage(byteBuffer,pool,customParserMap); + break; } break; case TypeBytes.MESSAGE_TYPE_PLAYER: diff --git a/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java b/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java index 96b71701..98f48b2a 100644 --- a/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java +++ b/src/main/java/electrosphere/net/parser/net/message/TypeBytes.java @@ -48,6 +48,7 @@ public class TypeBytes { */ public static final byte LORE_MESSAGE_TYPE_REQUESTRACES = 0; public static final byte LORE_MESSAGE_TYPE_RESPONSERACES = 1; + public static final byte LORE_MESSAGE_TYPE_TEMPORALUPDATE = 2; /* Lore packet sizes */ diff --git a/src/main/java/electrosphere/net/server/Server.java b/src/main/java/electrosphere/net/server/Server.java index 70c4fd0b..e01e5574 100644 --- a/src/main/java/electrosphere/net/server/Server.java +++ b/src/main/java/electrosphere/net/server/Server.java @@ -155,9 +155,7 @@ public class Server implements Runnable { public void broadcastMessage(NetworkMessage message){ connectListLock.acquireUninterruptibly(); for(ServerConnectionHandler client : activeConnections){ - if(Globals.clientState.clientPlayer == null || client.playerID != Globals.clientState.clientPlayer.getId()){ - client.addMessagetoOutgoingQueue(message); - } + client.addMessagetoOutgoingQueue(message); } connectListLock.release(); } diff --git a/src/main/java/electrosphere/net/server/protocol/LoreProtocol.java b/src/main/java/electrosphere/net/server/protocol/LoreProtocol.java index 6c626109..6c25e03f 100644 --- a/src/main/java/electrosphere/net/server/protocol/LoreProtocol.java +++ b/src/main/java/electrosphere/net/server/protocol/LoreProtocol.java @@ -22,6 +22,7 @@ public class LoreProtocol implements ServerProtocolTemplate { connectionHandler.addMessagetoOutgoingQueue(LoreMessage.constructResponseRacesMessage(returnData)); return null; } + case TEMPORALUPDATE: case RESPONSERACES: return message; } @@ -33,6 +34,7 @@ public class LoreProtocol implements ServerProtocolTemplate { switch(message.getMessageSubtype()){ case REQUESTRACES: case RESPONSERACES: + case TEMPORALUPDATE: //silently ignore break; } diff --git a/src/main/java/electrosphere/server/datacell/Realm.java b/src/main/java/electrosphere/server/datacell/Realm.java index 59b86ba9..e3884e7c 100644 --- a/src/main/java/electrosphere/server/datacell/Realm.java +++ b/src/main/java/electrosphere/server/datacell/Realm.java @@ -37,72 +37,67 @@ public class Realm { /** * The set containing all data cells loaded into this realm */ - Set loadedDataCells = new HashSet(); + private Set loadedDataCells = new HashSet(); /** * this is the cell that all players loading into the game (via connection startup, death, etc) reside in */ - ServerDataCell loadingCell = new ServerDataCell(new Scene()); + private ServerDataCell loadingCell = new ServerDataCell(new Scene()); /** * The data cell that will contain in-inventory items */ - ServerDataCell inventoryCell = new ServerDataCell(new Scene()); - - /** - * resolver for entity -> data cell within this realm - */ - EntityDataCellMapper entityDataCellMapper; + private ServerDataCell inventoryCell = new ServerDataCell(new Scene()); /** * provides functions for relating data cells to physical locations (eg creating cells, deleting cells, etc) */ - DataCellManager dataCellManager; + private DataCellManager dataCellManager; /** * The pathfinding manager */ - PathfindingManager pathfindingManager; + private PathfindingManager pathfindingManager; /** * Main entity physics collision checking engine */ - CollisionEngine collisionEngine; + private CollisionEngine collisionEngine; /** * The chemistry collision engine */ - CollisionEngine chemistryEngine; + private CollisionEngine chemistryEngine; /** * Hitbox manager for the realm */ - HitboxManager hitboxManager; + private HitboxManager hitboxManager; /** * The world data about the server */ - ServerWorldData serverWorldData; + private ServerWorldData serverWorldData; /** * The content manager */ - ServerContentManager serverContentManager; + private ServerContentManager serverContentManager; /** * The macro data for the realm */ - MacroData macroData; + private MacroData macroData; /** * The instanceId of the scene that was loaded with this realm */ - int sceneInstanceId = NO_SCENE_INSTANCE; + private int sceneInstanceId = NO_SCENE_INSTANCE; /** * The list of available spawnpoints */ - List spawnPoints = new LinkedList(); + private List spawnPoints = new LinkedList(); /** * Realm constructor diff --git a/src/main/java/electrosphere/server/macro/MacroData.java b/src/main/java/electrosphere/server/macro/MacroData.java index 30558c9b..36173925 100644 --- a/src/main/java/electrosphere/server/macro/MacroData.java +++ b/src/main/java/electrosphere/server/macro/MacroData.java @@ -5,6 +5,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import electrosphere.data.macro.temporal.MacroTemporalData; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; import electrosphere.server.datacell.ServerWorldData; @@ -82,6 +83,11 @@ public class MacroData { */ private MacroPathCache pathingCache = new MacroPathCache(); + /** + * The macro temporal data + */ + private MacroTemporalData temporalData = new MacroTemporalData(); + /** * Generates a world * @param seed The seed for the world @@ -396,5 +402,13 @@ public class MacroData { public MacroPathCache getPathCache(){ return this.pathingCache; } + + /** + * Gets the temporal data + * @return The temporal data + */ + public MacroTemporalData getTemporalData(){ + return this.temporalData; + } } diff --git a/src/main/java/electrosphere/server/simulation/MacroSimulation.java b/src/main/java/electrosphere/server/simulation/MacroSimulation.java index 9361318a..5a537ae1 100644 --- a/src/main/java/electrosphere/server/simulation/MacroSimulation.java +++ b/src/main/java/electrosphere/server/simulation/MacroSimulation.java @@ -9,6 +9,7 @@ import electrosphere.server.macro.civilization.town.Town; import electrosphere.server.macro.civilization.town.TownSimulator; import electrosphere.server.service.CharacterService; import electrosphere.server.simulation.chara.CharaSimulation; +import electrosphere.server.simulation.temporal.TemporalSimulator; /** * Performs the macro-level (ie virtual, non-physics based) simulation @@ -53,6 +54,11 @@ public class MacroSimulation { TownSimulator.simualte(town); } Globals.profiler.endCpuSample(); + + // + //temporal update + TemporalSimulator.simulate(realm); + Globals.profiler.endCpuSample(); } diff --git a/src/main/java/electrosphere/server/simulation/temporal/TemporalSimulator.java b/src/main/java/electrosphere/server/simulation/temporal/TemporalSimulator.java new file mode 100644 index 00000000..0f18fc71 --- /dev/null +++ b/src/main/java/electrosphere/server/simulation/temporal/TemporalSimulator.java @@ -0,0 +1,37 @@ +package electrosphere.server.simulation.temporal; + +import electrosphere.data.macro.temporal.MacroTemporalData; +import electrosphere.engine.Globals; +import electrosphere.net.parser.net.message.LoreMessage; +import electrosphere.server.datacell.Realm; +import electrosphere.util.SerializationUtils; + +/** + * Temporal macro data simulator + */ +public class TemporalSimulator { + + /** + * Number of temporal ticks per sim frame + */ + private static final int TEMPORAL_TICKS_PER_SIM_FRAME = 1; + + /** + * The rate at which to send synchronization packets to clients to update temporal data + */ + public static final int TEMPORAL_SYNC_RATE = 600; + + /** + * Simulates the temporal macro data + * @param macroData The macro data + */ + public static void simulate(Realm realm){ + MacroTemporalData temporalData = realm.getMacroData().getTemporalData(); + temporalData.increment(TemporalSimulator.TEMPORAL_TICKS_PER_SIM_FRAME); + if(temporalData.getTime() % TEMPORAL_SYNC_RATE == 0){ + String data = SerializationUtils.serialize(temporalData); + Globals.serverState.server.broadcastMessage(LoreMessage.constructTemporalUpdateMessage(data)); + } + } + +} diff --git a/src/main/java/electrosphere/util/math/BasicMathUtils.java b/src/main/java/electrosphere/util/math/BasicMathUtils.java new file mode 100644 index 00000000..7005ad75 --- /dev/null +++ b/src/main/java/electrosphere/util/math/BasicMathUtils.java @@ -0,0 +1,19 @@ +package electrosphere.util.math; + +/** + * Basic math functions + */ +public class BasicMathUtils { + + /** + * Linearly interpolates between two doubles + * @param a The first double + * @param b The second double + * @param percent The percentage to interpolate between them + * @return The interpolated value + */ + public static double lerp(double a, double b, double percent){ + return a * (1.0 - percent) + (b * percent); + } + +} diff --git a/src/net/lore.json b/src/net/lore.json index 55e30f9f..caf1bcaa 100644 --- a/src/net/lore.json +++ b/src/net/lore.json @@ -22,6 +22,13 @@ "data" : [ "data" ] + }, + { + "messageName" : "TemporalUpdate", + "description" : "Sends the current temporal data to the client", + "data" : [ + "data" + ] } ] }