From 061494c83f418d1b192a277801cc1d452ae9f656 Mon Sep 17 00:00:00 2001 From: reider745 Date: Thu, 16 Jul 2026 09:36:45 +0300 Subject: [PATCH 1/4] feat: merge changes --- .../innercore/api/NativeCallback.java | 4 +- .../innercore/api/NativeScales.java | 9 +- .../mod/adaptedscript/AdaptedScriptAPI.java | 28 +- .../innercore/api/runtime/Updatable.java | 4 + .../api/runtime/saver/CriticalDataFile.java | 18 +- .../runtime/saver/world/WorldDataSaver.java | 24 +- .../saver/world/WorldDataScopeRegistry.java | 67 +++- .../blockentity/BlockEntityRegistry.java | 2 +- .../icstd/inventory/ItemInstance.java | 4 +- .../icstd/inventory/ItemInstanceExtra.java | 5 +- .../innercore/icstd/network/INetPacket.java | 5 +- .../icstd/network/NetworkModule.java | 50 +-- .../icstd/scene/GameObjectHandle.java | 103 +++++-- .../icstd/scene/GameObjectManager.java | 291 ++++++++++-------- .../icstd/scene/GameObjectScope.java | 62 ++++ .../icstd/serialization/ObjectSerializer.java | 53 ++-- .../ScriptableObjectSerializer.java | 81 +++-- .../internal/serialization/JsonIO.java | 2 +- .../innercore/coreengine/core-engine.dev.js | 4 +- 19 files changed, 564 insertions(+), 252 deletions(-) create mode 100644 src/main/java/org/innercore/icstd/scene/GameObjectScope.java diff --git a/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java b/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java index 05e301c4..12367cd3 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java @@ -52,6 +52,7 @@ import org.innercore.icstd.region.BlockSource; import org.innercore.icstd.region.BlockState; import org.innercore.icstd.scene.GameObjectManager; +import org.innercore.icstd.scene.GameObjectScope; import org.mozilla.javascript.Context; import org.mozilla.javascript.Scriptable; @@ -326,7 +327,8 @@ public static void onGameStopped(boolean isServer) { // on server side save all data WorldDataSaverHandler.getInstance().onLevelLeft(); - GameObjectManager.clearAllDefaultServerScenes(false); + GameObjectManager.clearAllDefaultServerScenes(true, false); + GameObjectScope.SERVER_SCOPE.expireAllHandles(); // legacy callback for server left if (callLegacyCallback) { diff --git a/src/main/java/com/zhekasmirnov/innercore/api/NativeScales.java b/src/main/java/com/zhekasmirnov/innercore/api/NativeScales.java index 3f3c8544..3cdc2db8 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/NativeScales.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/NativeScales.java @@ -57,7 +57,14 @@ public void read(IScopedReader io, SerializationContext ctx) { return; } for (String mobKey : allScales.iterKeys()) { - final long mob = Long.parseLong(mobKey); + final long mob; + try { + mob = Long.parseLong(mobKey); + } catch (NumberFormatException e) { + ctx.addError("invalid mob uid key in scales scope: " + mobKey); + continue; + } + IObjectReader mobScales = allScales.beginScope(mobKey).asObject(); if (mobScales != null) { for (String scaleId : mobScales.iterKeys()) { diff --git a/src/main/java/com/zhekasmirnov/innercore/api/mod/adaptedscript/AdaptedScriptAPI.java b/src/main/java/com/zhekasmirnov/innercore/api/mod/adaptedscript/AdaptedScriptAPI.java index 0da7a50c..83170017 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/mod/adaptedscript/AdaptedScriptAPI.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/mod/adaptedscript/AdaptedScriptAPI.java @@ -61,6 +61,7 @@ import org.innercore.icstd.entity.PathNavigation; import org.innercore.icstd.inventory.ItemInstance; import org.innercore.icstd.inventory.ItemInstanceExtra; +import org.innercore.icstd.serialization.ObjectSerializer; import org.innercore.icstd.serialization.SerializationContext; import org.json.JSONException; import org.mozilla.javascript.*; @@ -2194,7 +2195,13 @@ public static String registerObjectSaver(final String name, Object scopeSaver) { ObjectSaverRegistry.registerSaver(name, new ObjectSaver() { @Override public Object read(ScriptableObject input) { - return saver.read(input); + Object result = saver.read(input); + // rhino coerces js undefined into the string "undefined" for object-typed + // interface methods, read() returning undefined means null + if ("undefined".equals(result)) { + return null; + } + return result; } @Override @@ -2218,10 +2225,25 @@ public static Object deserializeFromString(String param) { } @JSStaticFunction - public static void registerObject(Object obj, String saverId) { - ObjectSaverRegistry.registerObject(obj, saverId); + public static void registerObject(Object obj, Object saverId) { + String typeId; + if (saverId instanceof Number) { + // legacy numeric saver id, e.g. stored by an old mod + typeId = ObjectSerializer.getTypeIdFromLegacyHashId(((Number) saverId).intValue(), null); + } else { + typeId = saverId != null ? saverId.toString() : null; + } + ObjectSaverRegistry.registerObject(obj, typeId); } + @Deprecated + @JSStaticFunction + public static Object getObjectSaverId(Object obj) { + String typeId = ObjectSerializer.getSerializerTypeIdFromObject(obj); + return typeId != null ? typeId : -1; + } + + @JSStaticFunction public static void setObjectIgnored(ScriptableObject obj, boolean ignore) { // NOTE: This was not working for a long time already diff --git a/src/main/java/com/zhekasmirnov/innercore/api/runtime/Updatable.java b/src/main/java/com/zhekasmirnov/innercore/api/runtime/Updatable.java index 29d56a57..191d54e4 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/runtime/Updatable.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/runtime/Updatable.java @@ -203,6 +203,10 @@ public void write(IScopedWriter io, SerializationContext ctx) { @Override public void read(IScopedReader io, SerializationContext ctx) { IArrayReader array = io.asArray(); + if (array == null) { + io.endScope(); + return; + } int successfullyLoaded = 0; for (int i = 0; i < array.size(); i++) { Object possibleUpdatable = ObjectSerializer.deserializeObject(array.beginScope(i), Object.class, ctx, true); diff --git a/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/CriticalDataFile.java b/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/CriticalDataFile.java index 841931db..d7ba76bf 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/CriticalDataFile.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/CriticalDataFile.java @@ -33,14 +33,21 @@ public synchronized File readBackupAfterMainFailed() { return null; if (state != State.READING) throw new IllegalStateException("readBackupAfterMainFailed(): not in correct state " + state); - mainFile.delete(); + // quarantine the corrupt main file for manual recovery - it must not stay in place, or the + // next successful write would rotate it into the backup + File corruptFile = new File(mainFile.getAbsolutePath() + ".corrupt"); + corruptFile.delete(); + if (!mainFile.renameTo(corruptFile)) { + mainFile.delete(); + } + state = State.READING_BACKUP; return backupFile; } public synchronized void endRead() { if (state != State.READING && state != State.READING_BACKUP) - throw new IllegalStateException("endRead(): not in correct state " + state); + return; state = State.IDLE; } @@ -51,10 +58,10 @@ public synchronized File beginWrite() { return tmpFile; } + // NOTE: on failure the state remains WRITING and the caller must call abortWrite() public synchronized boolean endSuccessfulWrite(boolean writeToLog) { if (state != State.WRITING) throw new IllegalStateException("endSuccessfulWrite(): not in correct state " + state); - state = State.IDLE; if (mainFile.isFile()) { // main file is valid backupFile.delete(); if (!mainFile.renameTo(backupFile)) { @@ -66,17 +73,18 @@ public synchronized boolean endSuccessfulWrite(boolean writeToLog) { return false; } if (tmpFile.renameTo(mainFile)) { + state = State.IDLE; if (writeToLog) ICLog.d("SAVER", "renamed saves file " + tmpFile.getAbsolutePath() + " to " + mainFile.getName()); return true; } else { - if (writeToLog) ICLog.d("SAVER", "failed to rename " + mainFile.getAbsolutePath() + " to " + backupFile.getName()); + if (writeToLog) ICLog.d("SAVER", "failed to rename " + tmpFile.getAbsolutePath() + " to " + mainFile.getName()); return false; } } public synchronized void abortWrite() { if (state != State.WRITING) - throw new IllegalStateException("abortWrite(): not in correct state " + state); + return; state = State.IDLE; tmpFile.delete(); } diff --git a/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataSaver.java b/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataSaver.java index 319b16c2..c23722a7 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataSaver.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataSaver.java @@ -32,6 +32,8 @@ public enum SaverState { private final File worldDirectory; private final CriticalDataFile savesFile; private final Map missingScopeData = new HashMap<>(); + private final WorldDataScopeRegistry.RetainedScopeData retainedScopeData = new WorldDataScopeRegistry.RetainedScopeData(); + private SaverState state = SaverState.IDLE; private boolean savingErrorsReportedAsDialog = false; @@ -79,12 +81,19 @@ public synchronized void readAllData(boolean showLogOnError) { return; } savingErrorsReportedAsDialog = false; + missingScopeData.clear(); + retainedScopeData.clear(); - JSONObject json = readJsonImpl(savesFile.beginRead()); - if (json == null) { - json = readJsonImpl(savesFile.readBackupAfterMainFailed()); + JSONObject json; + try { + json = readJsonImpl(savesFile.beginRead()); + if (json == null) { + json = readJsonImpl(savesFile.readBackupAfterMainFailed()); + } + } finally { + savesFile.endRead(); } - savesFile.endRead(); + if (json == null) { json = readJsonWithLockCheck(worldDirectory, LEGACY_MAIN_SAVES_BACKUP, false); } @@ -93,9 +102,8 @@ public synchronized void readAllData(boolean showLogOnError) { } state = SaverState.READ; - missingScopeData.clear(); SerializationContext ctx = new SerializationContext(); - WorldDataScopeRegistry.getInstance().readAllScopes(json, ctx, missingScopeData); + WorldDataScopeRegistry.getInstance().readAllScopes(json, ctx, retainedScopeData, missingScopeData); state = SaverState.IDLE; if (showLogOnError) { @@ -118,11 +126,11 @@ public synchronized void saveAllData(boolean showLogOnError) { state = SaverState.SAVE; JSONObject json = new JSONObject(missingScopeData); SerializationContext ctx = new SerializationContext(); - WorldDataScopeRegistry.getInstance().saveAllScopes(json, ctx); + WorldDataScopeRegistry.getInstance().saveAllScopes(json, ctx, retainedScopeData); state = SaverState.IDLE; - File fileToWrite = savesFile.beginWrite(); try { + File fileToWrite = savesFile.beginWrite(); ICLog.i("SAVER", "writing saves to " + fileToWrite.getAbsolutePath()); FileUtils.writeJSON(fileToWrite, json); if (!savesFile.endSuccessfulWrite(true)) diff --git a/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataScopeRegistry.java b/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataScopeRegistry.java index 09b46a46..6eccaecb 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataScopeRegistry.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/runtime/saver/world/WorldDataScopeRegistry.java @@ -1,17 +1,17 @@ package com.zhekasmirnov.innercore.api.runtime.saver.world; +import com.zhekasmirnov.innercore.api.log.ICLog; import com.zhekasmirnov.innercore.api.mod.ScriptableObjectHelper; import com.zhekasmirnov.innercore.api.runtime.Callback; import org.innercore.icstd.serialization.IScopedReader; import org.innercore.icstd.serialization.IScopedWriter; import org.innercore.internal.serialization.JsonIO; import org.innercore.icstd.serialization.SerializationContext; +import org.json.JSONException; import org.json.JSONObject; import org.mozilla.javascript.ScriptableObject; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import java.util.*; public class WorldDataScopeRegistry { private static final WorldDataScopeRegistry instance = new WorldDataScopeRegistry(); @@ -45,6 +45,19 @@ public interface SaverScope { void write(IScopedWriter io, SerializationContext ctx); } + // per-world backup of raw scope data, used to keep the last known copy of a scope in the saves + // file when the scope fails to read or write, instead of dropping or overwriting it with defaults + public static class RetainedScopeData { + private final Map lastKnownData = new HashMap<>(); + private final Set failedToRead = new HashSet<>(); + + public void clear() { + lastKnownData.clear(); + failedToRead.clear(); + } + } + + private final Map scopeMap = new HashMap<>(); @@ -53,20 +66,30 @@ public void addScope(String name, SaverScope scope) { return; } while (scopeMap.containsKey(name)) { - name += name.hashCode() & 0xFF; + String renamed = name + (name.hashCode() & 0xFF); + ICLog.e("SAVER", "saves scope name collision: '" + name + "' is already registered, new scope will be registered as '" + renamed + "' and its data will depend on registration order"); + name = renamed; } scopeMap.put(name, scope); } - public void readAllScopes(JSONObject json, SerializationContext ctx, Map outMissingScopes) { + public void readAllScopes(JSONObject json, SerializationContext ctx, RetainedScopeData retained, Map outMissingScopes) { for (Map.Entry entry : scopeMap.entrySet()) { String key = entry.getKey(); Object data = json.opt(key); + if (data != null) { + retained.lastKnownData.put(key, data); + } JsonIO.ScopedReader reader = null; try { reader = new JsonIO.ScopedReader(data); entry.getValue().read(reader, ctx); } catch (Throwable err) { + // a scope is marked as failed only when it had data to lose - a reader failing on + // empty data must not suppress saving fresh data + if (data != null) { + retained.failedToRead.add(key); + } ctx.addError(err, data, "global saves scope failed to read: " + key); } if (reader != null) { @@ -82,16 +105,48 @@ public void readAllScopes(JSONObject json, SerializationContext ctx, Map entry : scopeMap.entrySet()) { + String key = entry.getKey(); + if (retained.failedToRead.contains(key)) { + // the scope never received its saved data, writing it would replace the data with + // defaults - keep the last known copy instead + Object lastKnown = retained.lastKnownData.get(key); + if (lastKnown != null) { + ICLog.i("SAVER", "scope " + key + " failed to read on world load, keeping its previous data"); + putJsonSafe(json, key, lastKnown); + } + continue; + } try { JsonIO.ScopedWriter writer = new JsonIO.ScopedWriter(); entry.getValue().write(writer, ctx); writer.forceEndScope(ctx); json.put(entry.getKey(), writer.getResult()); + Object result = writer.getResult(); + putJsonSafe(json, key, result); + if (result != null) { + retained.lastKnownData.put(key, result); + } else { + retained.lastKnownData.remove(key); + } } catch (Throwable err) { ctx.addError(err, null, "global saves scope failed to read: " + entry.getKey()); + ctx.addError(err, null, "global saves scope failed to write: " + key); + // don't lose the scope - write the data from the last successful read or write + Object lastKnown = retained.lastKnownData.get(key); + if (lastKnown != null) { + putJsonSafe(json, key, lastKnown); + } } } } + } diff --git a/src/main/java/org/innercore/icstd/blockentity/BlockEntityRegistry.java b/src/main/java/org/innercore/icstd/blockentity/BlockEntityRegistry.java index 3f8e9fb6..0b29770e 100644 --- a/src/main/java/org/innercore/icstd/blockentity/BlockEntityRegistry.java +++ b/src/main/java/org/innercore/icstd/blockentity/BlockEntityRegistry.java @@ -125,7 +125,7 @@ public static BlockEntityComponent createForBlockAndPlaceAtScene(GameObjectManag public static BlockSource initBlockEntity(BlockEntityComponent blockEntity) { GameObjectManager scene = blockEntity.getObjectHandle().getScene(); - if (scene == null || scene.isDefaultScene()) { + if (scene == null || !scene.isDefaultScene()) { throw new IllegalStateException("invalid scene " + scene + " for block entity " + blockEntity); } if (scene.isServerSide()) { diff --git a/src/main/java/org/innercore/icstd/inventory/ItemInstance.java b/src/main/java/org/innercore/icstd/inventory/ItemInstance.java index a07a6323..d5f732c0 100644 --- a/src/main/java/org/innercore/icstd/inventory/ItemInstance.java +++ b/src/main/java/org/innercore/icstd/inventory/ItemInstance.java @@ -37,7 +37,7 @@ public ItemInstance(int id, int count, int data) { } @ApiBinding - public ItemInstance(ItemInstance rhs) { + public ItemInstance(@Nullable ItemInstance rhs) { if (rhs == null) return; this.id = rhs.id; this.count = rhs.count; @@ -84,7 +84,7 @@ public static ItemInstance makeFrom(Item item) { } @ApiBinding - public static ItemInstance parse(Object obj, boolean nullable) { + public static ItemInstance parse(@Nullable Object obj, boolean nullable) { while (obj instanceof Wrapper) { obj = ((Wrapper) obj).unwrap(); } diff --git a/src/main/java/org/innercore/icstd/inventory/ItemInstanceExtra.java b/src/main/java/org/innercore/icstd/inventory/ItemInstanceExtra.java index d7a38752..1f8ecf3d 100644 --- a/src/main/java/org/innercore/icstd/inventory/ItemInstanceExtra.java +++ b/src/main/java/org/innercore/icstd/inventory/ItemInstanceExtra.java @@ -103,10 +103,13 @@ public ScriptableObject save(Object input) { private static final String saverId = ObjectSerializer.registerType("ic:ii_ext", ItemInstanceExtra.class, new IObjectSerializer() { @Override public ItemInstanceExtra read(IScopedReader reader, SerializationContext ctx, String typeId) { - if (reader.isEmpty()) + if (reader.isEmpty()) { + reader.endScope(); return null; + } if (!reader.isObject()) { ctx.addError("ItemInstanceExtra invalid format"); + reader.endScope(); return null; } ItemInstanceExtra extra = new ItemInstanceExtra(); diff --git a/src/main/java/org/innercore/icstd/network/INetPacket.java b/src/main/java/org/innercore/icstd/network/INetPacket.java index 3c66eb6e..7ceae7f2 100644 --- a/src/main/java/org/innercore/icstd/network/INetPacket.java +++ b/src/main/java/org/innercore/icstd/network/INetPacket.java @@ -1,6 +1,9 @@ package org.innercore.icstd.network; public interface INetPacket { - String getPacketType(); // must be a valid serializer id + // must be a valid serializer id, registered with an object type implementing INetPacket, + // packets of other types are not deserialized on receive + String getPacketType(); + void process(NetworkClient fromClient, String meta); } diff --git a/src/main/java/org/innercore/icstd/network/NetworkModule.java b/src/main/java/org/innercore/icstd/network/NetworkModule.java index 3b39721e..be642201 100644 --- a/src/main/java/org/innercore/icstd/network/NetworkModule.java +++ b/src/main/java/org/innercore/icstd/network/NetworkModule.java @@ -19,25 +19,28 @@ public class NetworkModule { private static final HashMap> clientPacketListeners = new HashMap<>(); private static final HashMap> serverPacketListeners = new HashMap<>(); + private static void sendPacketData(boolean isServer, NetworkClient toClient, String name, Object data) { + if (isServer) + ThreadTypeMarker.assertServerThread(); + else + ThreadTypeMarker.assertClientThread(); + if (isServer) { + if (toClient == null) { + Network.getSingleton().getServer().sendToAll(name, data); + } else { + toClient.impl.send(name, data); + } + } else { + Network.getSingleton().getClient().send(name, data); + } + } + private static IScopedWriter sendPacketImpl(boolean isServer, NetworkClient toClient, String name) { return new JsonIO.ScopedWriter() { @Override public void endScope() { super.endScope(); - Object data = getResult(); - if (isServer) - ThreadTypeMarker.assertServerThread(); - else - ThreadTypeMarker.assertClientThread(); - if (isServer) { - if (toClient == null) { - Network.getSingleton().getServer().sendToAll(name, data); - } else { - toClient.impl.send(name, data); - } - } else { - Network.getSingleton().getClient().send(name, data); - } + sendPacketData(isServer, toClient, name, getResult()); } }; } @@ -54,7 +57,10 @@ private static void sendPacketImpl(boolean isServer, NetworkClient toClient, INe } else if (!ObjectSerializer.isTypeRegistered(typeId)) { throw new IllegalStateException("INetPacket type is not registered: " + typeId); } - ObjectSerializer.serializeObjectUnpacked(sendPacketImpl(isServer, toClient, typeId), packet, typeId, ctx, true); + JsonIO.ScopedWriter writer = new JsonIO.ScopedWriter(); + if (ObjectSerializer.serializeObjectUnpacked(writer, packet, typeId, ctx, true)) + sendPacketData(isServer, toClient, typeId, writer.getResult()); + // TODO: report error } public static IScopedWriter sendToServer(String name) { @@ -115,12 +121,12 @@ public static void removeServerPacketListener(String name, IPacketListener liste @ApiHidden public static void processPacketImpl(NetworkClient fromClient, String name, Object data, String meta) { - if (ObjectSerializer.getSerializerRecordByTypeId(name) == null) + ObjectSerializer.SerializerRecord record = ObjectSerializer.getSerializerRecordByTypeId(name); + if (record == null) return; - SerializationContext ctx = new SerializationContext(); if (data instanceof JSONObject || data instanceof JSONArray || data == null) { - Object maybePacket = ObjectSerializer.deserializeObjectUnpacked(new JsonIO.ScopedReader(data), name, Object.class, ctx, true); (fromClient != null ? Network.getSingleton().getServerThreadJobExecutor() : Network.getSingleton().getClientThreadJobExecutor()).add(() -> { + SerializationContext ctx = new SerializationContext(); List listeners = (fromClient != null ? serverPacketListeners : clientPacketListeners).get(name); if (listeners != null) { for (IPacketListener listener : listeners) { @@ -129,8 +135,12 @@ public static void processPacketImpl(NetworkClient fromClient, String name, Obje io.forceEndScope(ctx); } } - if (maybePacket instanceof INetPacket) { - ((INetPacket) maybePacket).process(fromClient, meta); + // run deserializer on the correct thread + if (INetPacket.class.isAssignableFrom(record.getObjectType())) { + Object maybePacket = ObjectSerializer.deserializeObjectUnpacked(new JsonIO.ScopedReader(data), name, Object.class, ctx, true); + if (maybePacket instanceof INetPacket) { + ((INetPacket) maybePacket).process(fromClient, meta); + } } }); } diff --git a/src/main/java/org/innercore/icstd/scene/GameObjectHandle.java b/src/main/java/org/innercore/icstd/scene/GameObjectHandle.java index c5cbc65d..3ce7bc28 100644 --- a/src/main/java/org/innercore/icstd/scene/GameObjectHandle.java +++ b/src/main/java/org/innercore/icstd/scene/GameObjectHandle.java @@ -7,29 +7,48 @@ import org.innercore.icstd.script.ScriptObjectComponent; +// NOTE: hashCode and equals are deliberately not overridden! handles are unique objects stored in a corresponding scope map @ApiBinding public class GameObjectHandle extends GameObjectHandleBindGenBase { - final GameObjectManager scene; - final long rawHandle; + final long value; + final GameObjectScope scope; + GameObjectManager scene = null; + int slotIdx = -1; + boolean isExpired = false; + + GameObjectHandle(GameObjectScope scope, long handle) { + this.scope = scope; + this.value = handle; + } - GameObjectHandle(GameObjectManager scene, long handle) { - this.scene = scene; - this.rawHandle = handle; + @ApiBinding + public GameObjectManager getScene() { + return scene; } @ApiBinding - public GameObjectHandle placeAsGlobal() { - scene.placeAsGlobal(rawHandle); - return this; + public long getHandleValue() { + return value; + } + + public GameObjectScope getScope() { + return scope; } @ApiBinding - public GameObjectManager getScene() { - return scene; + public GameObjectHandle placeAsGlobal() { + if (slotIdx < 0) { + throw new IllegalStateException("placeAsGlobal on dead handle"); + } + scene.placeAsGlobal(slotIdx); + return this; } public GameObjectHandle placeAtBlockPos(BlockPos pos) { - scene.placeAtBlockPos(rawHandle, pos); + if (slotIdx < 0) { + throw new IllegalStateException("placeAtBlockPos on dead handle"); + } + scene.placeAtBlockPos(slotIdx, pos); return this; } @@ -38,52 +57,76 @@ public GameObjectHandle placeAtBlockPos(int x, int y, int z) { return placeAtBlockPos(new BlockPos(x, y, z)); } + @ApiBinding + public GameObjectHandle transferToScene(GameObjectManager toScene) { + if (slotIdx < 0) { + throw new IllegalStateException("transferTo on dead handle"); + } + if (scene == toScene) { + return this; + } + if (toScene.getObjectScope() != scope) { + throw new IllegalArgumentException("transferTo scene with different scope"); + } + Object o = scene.unlinkObject(slotIdx); + assert slotIdx < 0; // must be unlinked at this point + toScene.createImpl(this, o); + assert slotIdx >= 0; // verify it is re-linked again + return this; + } + @ApiBinding public GameObjectHandle setObject(Object object) { - scene.setObject(rawHandle, object); + if (slotIdx < 0) { + throw new IllegalStateException("placeAtBlockPos on dead handle"); + } + scene.setObject(slotIdx, object); return this; } @ApiBinding - public boolean destroyNow() { return scene.destroyNow(rawHandle); } + public boolean destroyNow() { + return slotIdx >= 0 && scene.destroyNow(slotIdx); + } @ApiBinding - public void destroyDeferred() { scene.destroyDeferred(rawHandle); } + public void destroyDeferred() { + if (slotIdx >= 0) + scene.destroyDeferred(slotIdx); + } - public Object discard() { return scene.discard(rawHandle); } + public Object discardNow() { return slotIdx >= 0 ? scene.discardNow(slotIdx) : null; } - @ApiBinding("discard") - public Object discardForScript() { - Object discarded = discard(); + @ApiBinding("discardNow") + public Object discardNowForScript() { + Object discarded = discardNow(); return discarded instanceof ScriptObjectComponent ? ((ScriptObjectComponent) discarded).getScriptObject() : discarded; } @ApiBinding - public boolean isAlive() { return scene.isAlive(rawHandle); } + public boolean isExpired() { return isExpired; } + + @ApiBinding + public boolean isAlive() { return slotIdx >= 0; } @ApiBinding - public boolean isLoaded() { return scene.isLoaded(rawHandle); } + public boolean isLoaded() { return slotIdx >= 0 && scene.isLoaded(slotIdx); } - public BlockPos getBlockPos() { return scene.getBlockPos(rawHandle); } + public BlockPos getBlockPos() { return slotIdx >= 0 ? scene.getBlockPos(slotIdx) : null; } @ApiBinding("getBlockPos") public GenericScriptPos3 getBlockPosForScript() { return new GenericScriptPos3(getBlockPos()); } - public Object getObject() { return scene.getObject(rawHandle); } + public Object getObject() { return slotIdx >= 0 ? scene.getObject(slotIdx) : null; } @ApiBinding("getObject") public Object getObjectForScript() { - Object object = scene.getObject(rawHandle); + Object object = getObject(); return object instanceof ScriptObjectComponent ? ((ScriptObjectComponent) object).getScriptObject() : object; } @Override - public boolean equals(Object object) { - if (object == null || getClass() != object.getClass()) return false; - GameObjectHandle that = (GameObjectHandle) object; - return rawHandle == that.rawHandle; + public String toString() { + return "[Handle " + value + "]"; } - - @Override - public int hashCode() { return (int) rawHandle; } } diff --git a/src/main/java/org/innercore/icstd/scene/GameObjectManager.java b/src/main/java/org/innercore/icstd/scene/GameObjectManager.java index 8b310d25..6be0fdf6 100644 --- a/src/main/java/org/innercore/icstd/scene/GameObjectManager.java +++ b/src/main/java/org/innercore/icstd/scene/GameObjectManager.java @@ -22,13 +22,14 @@ public class GameObjectManager extends GameObjectManagerBindGenBase { private static final int STATE_LAZY_LOADED = 1; private static final int STATE_LOADED = 2; + private final GameObjectScope objectScope; private final String uniqueName; private int dimensionId = -1; - private boolean isServerSide = true; private boolean isDefaultScene = false; private boolean isScopeLoaded = false; - public GameObjectManager(String uniqueName) { + public GameObjectManager(GameObjectScope scope, String uniqueName) { + this.objectScope = scope; this.uniqueName = uniqueName; } @@ -41,11 +42,6 @@ public GameObjectManager setDimensionId(int dimensionId) { return this; } - public GameObjectManager setServerSide(boolean isServerSide) { - this.isServerSide = isServerSide; - return this; - } - GameObjectManager setDefaultScene(boolean isDefaultScene) { this.isDefaultScene = isDefaultScene; return this; @@ -56,37 +52,55 @@ public int getDimensionId() { } public boolean isServerSide() { - return isServerSide; + return objectScope.isServerSide(); } public boolean isDefaultScene() { return isDefaultScene; } + public GameObjectScope getObjectScope() { + return objectScope; + } + + // ADD/UPDATE - private static final AtomicLong nextUniqueHandleValue = new AtomicLong(1); + GameObjectHandle createImpl(GameObjectHandle handle, Object object) { + if (handle.isExpired()) { + throw new IllegalStateException("handle is expired " + handle); + } + if (handle.isAlive()) { + throw new IllegalStateException("handle is already alive"); + } + if (handle.scope != objectScope) { + throw new IllegalStateException("handle is from different scope"); + } + int slotIdx = allocateSlot(); + slots[slotIdx].handle = handle; + handle.slotIdx = slotIdx; + handle.scene = this; + setSlotObjectImpl(slotIdx, object, SetObjectActionType.DESTROY); + markObjectForUpdate(slotIdx); + return handle; + } @ApiBinding public GameObjectHandle create(Object object) { - GameObjectHandle handle = new GameObjectHandle(this, nextUniqueHandleValue.getAndAdd(1)); - int slotIdx = allocateSlot(); - handleToSlotIdx.put(handle.rawHandle, slotIdx); - slots[slotIdx].handle = handle.rawHandle; - setSlotObject(slotIdx, object); - markObjectForUpdate(slotIdx); + GameObjectHandle handle = objectScope.createUniqueHandle(); + createImpl(handle, object); return handle; } - void placeAsGlobal(long rawHandle) { - updateObjectPosImpl(rawHandle, slot -> { + void placeAsGlobal(int slotIdx) { + updateObjectPosImpl(slotIdx, slot -> { slot.posType = POS_TYPE_GLOBAL; }); } - void placeAtBlockPos(long rawHandle, BlockPos pos) { - updateObjectPosImpl(rawHandle, slot -> { + void placeAtBlockPos(int slotIdx, BlockPos pos) { + updateObjectPosImpl(slotIdx, slot -> { slot.posType = POS_TYPE_BLOCK; slot.x = pos.x; slot.y = pos.y; @@ -94,12 +108,7 @@ void placeAtBlockPos(long rawHandle, BlockPos pos) { }); } - private void updateObjectPosImpl(long rawHandle, Consumer slotConsumer) { - Integer slotIdxOpt = handleToSlotIdx.get(rawHandle); - if (slotIdxOpt == null) { - return; - } - int slotIdx = slotIdxOpt; + private void updateObjectPosImpl(int slotIdx, Consumer slotConsumer) { ObjectSlot slot = slots[slotIdx]; BlockPos prevBlockPos = slot.getBlockPos(); ChunkAABB prevAABB = slot.getChunkAABB(); @@ -109,39 +118,31 @@ private void updateObjectPosImpl(long rawHandle, Consumer slotConsum markObjectForUpdate(slotIdx); } - Object discard(long rawHandle) { - Integer slotIdxOpt = handleToSlotIdx.remove(rawHandle); - if (slotIdxOpt == null) { - return null; - } - return discardSlotObject(slotIdxOpt); + Object discardNow(int slotIdx) { + return unlinkSlotImpl(slotIdx, SetObjectActionType.DISCARD); } - boolean destroyNow(long rawHandle) { - Integer slotIdxOpt = handleToSlotIdx.remove(rawHandle); - if (slotIdxOpt == null) { - return false; - } - destroySlotImpl(slotIdxOpt); + boolean destroyNow(int slotIdx) { + unlinkSlotImpl(slotIdx, SetObjectActionType.DISCARD); return true; } - void destroyDeferred(long rawHandle) { - Integer slotIdxOpt = handleToSlotIdx.remove(rawHandle); - if (slotIdxOpt == null) { - return; - } - int slotIdx = slotIdxOpt; + Object unlinkObject(int slotIdx) { + return unlinkSlotImpl(slotIdx, SetObjectActionType.UNLINK); + } + + void destroyDeferred(int slotIdx) { slots[slotIdx].markedForDestroy = true; markObjectForUpdate(slotIdx); } - private void destroySlotImpl(int slotIdx) { + private Object unlinkSlotImpl(int slotIdx, SetObjectActionType action) { ObjectSlot slot = slots[slotIdx]; - setSlotObject(slotIdx, null); + Object o = setSlotObjectImpl(slotIdx, null, action); updateObjectBlockPos(slotIdx, slot.getBlockPos(), null); updateObjectAABB(slotIdx, slot.getChunkAABB(), null); - freeSlot(slotIdx); + freeSlot(slotIdx, action != SetObjectActionType.UNLINK); + return o; } // SCENE LEVEL STATE @@ -180,14 +181,17 @@ public boolean isScopeLoaded() { } @ApiBinding - public void clearAllObjects(boolean callDestroyForObjects) { + public void clearAllObjects(boolean runLifeCycleCallbacks, boolean isDestroy) { for (int slotIdx = 0; slotIdx < slotsSize; slotIdx++) { - if (callDestroyForObjects) { - setSlotObject(slotIdx, null); - } else { - discardSlotObject(slotIdx); - } + GameObjectHandle handle = slots[slotIdx].handle; + if (handle == null) + continue; + if (runLifeCycleCallbacks) + setSlotObjectImpl(slotIdx, null, isDestroy ? SetObjectActionType.DESTROY : SetObjectActionType.DISCARD); + objectScope.expireHandle(handle); } + invalidateQueryGeneration(); + slots = new ObjectSlot[0]; slotsSize = 0; slotsFreeListHead = -1; @@ -196,7 +200,6 @@ public void clearAllObjects(boolean callDestroyForObjects) { periodicChecksListSize = 0; periodicChecksListNextIdx = 0; - handleToSlotIdx.clear(); chunks.clear(); lookupByPos.clear(); chunksMarkedForUpdate.clear(); @@ -218,39 +221,27 @@ public void markChunkForUpdate(ChunkPos pos) { // GETTERS - boolean isAlive(long handle) { - return handleToSlotIdx.containsKey(handle); - } - - boolean isLoaded(long handle) { - Integer slotIdxOpt = handleToSlotIdx.get(handle); - return slotIdxOpt != null && slots[slotIdxOpt].isObjectLoaded; + boolean isLoaded(int slotIdx) { + return slots[slotIdx].isObjectLoaded; } - Object getObject(long handle) { - Integer slotIdxOpt = handleToSlotIdx.get(handle); - return slotIdxOpt == null ? null : slots[slotIdxOpt].object; + Object getObject(int slotIdx) { + return slots[slotIdx].object; } - BlockPos getBlockPos(long handle) { - Integer slotIdxOpt = handleToSlotIdx.get(handle); - return slotIdxOpt == null ? null : slots[slotIdxOpt].getBlockPos(); + BlockPos getBlockPos(int slotIdx) { + return slots[slotIdx].getBlockPos(); } - boolean setObject(long handle, Object object) { - Integer slotIdxOpt = handleToSlotIdx.get(handle); - if (slotIdxOpt == null) { - return false; - } - setSlotObject(slotIdxOpt, object); - markObjectForUpdate(slotIdxOpt); - return true; + void setObject(int slotIdx, Object object) { + setSlotObjectImpl(slotIdx, object, SetObjectActionType.DESTROY); + if (object != null) + markObjectForUpdate(slotIdx); } // QUERY - private abstract class BaseQueryImpl implements GameObjectQuery { @Override public boolean isEmpty() { @@ -268,15 +259,18 @@ public T one() { public GameObjectHandle oneHandle() { int i = searchSlot(firstSlot()); if (i == -1) return null; - return new GameObjectHandle(GameObjectManager.this, slots[i].handle); + return slots[i].handle; } @Override public Iterator iterator() { return new Iterator() { + final int gen = queryValidationGeneration; int i = firstSlot(); - @Override public boolean hasNext() { i = searchSlot(i); return i != -1; } + @Override public boolean hasNext() { validateQueryGeneration(gen); i = searchSlot(i); return i != -1; } @Override public T next() { + validateQueryGeneration(gen); + i = searchSlot(i); if (i == -1) return null; ObjectSlot slot = slots[i]; i = getNextSlot(i); @@ -288,13 +282,16 @@ public Iterator iterator() { @Override public Iterator iterHandles() { return new Iterator() { + final int gen = queryValidationGeneration; int i = firstSlot(); - @Override public boolean hasNext() { i = searchSlot(i); return i != -1; } + @Override public boolean hasNext() { validateQueryGeneration(gen); i = searchSlot(i); return i != -1; } @Override public GameObjectHandle next() { + validateQueryGeneration(gen); + i = searchSlot(i); if (i == -1) return null; ObjectSlot slot = slots[i]; i = getNextSlot(i); - return new GameObjectHandle(GameObjectManager.this, slot.handle); + return slot.handle; } }; } @@ -315,7 +312,7 @@ public List toHandleList() { List list = new ArrayList<>(); int i = searchSlot(firstSlot()); while (i != -1) { - list.add(new GameObjectHandle(GameObjectManager.this, slots[i].handle)); + list.add(slots[i].handle); i = searchSlot(getNextSlot(i)); } return list; @@ -326,13 +323,31 @@ public List toHandleList() { protected abstract int getNextSlot(int idx); } + private int queryValidationGeneration = 0; + + private void invalidateQueryGeneration() { + queryValidationGeneration++; + } + + private void validateQueryGeneration(int gen) { + if (queryValidationGeneration != gen) { + throw new ConcurrentModificationException("game object was destroyed/discarded/moved during query"); + } + } + public GameObjectQuery queryAtBlock(BlockPos pos) { return new BaseQueryImpl() { @Override protected int firstSlot() { Integer idx = lookupByPos.get(pos); return idx != null ? idx : -1; } - @Override protected int searchSlot(int idx) { return idx; } + @Override protected int searchSlot(int i) { + ObjectSlot slot; + while (i != -1 && ((slot = slots[i]).object) == null) { + i = slot.nextAtPos; + } + return i; + } @Override protected int getNextSlot(int idx) { return slots[idx].nextAtPos; } }; } @@ -371,7 +386,7 @@ public GameObjectQuery queryGlobal(Class cls) { return new BaseQueryImpl() { @Override protected int firstSlot() { return 0; } @Override protected int searchSlot(int i) { - for (; i < slotsSize && (slots[i] == null || slots[i].posType != POS_TYPE_GLOBAL || cls.isInstance(slots[i].object)); i++); + for (; i < slotsSize && (slots[i] == null || slots[i].posType != POS_TYPE_GLOBAL || !cls.isInstance(slots[i].object)); i++); return i < slotsSize ? i : -1; } @Override protected int getNextSlot(int idx) { return idx + 1; } @@ -406,23 +421,30 @@ private int allocateSlot() { return idx; } - private void freeSlot(int idx) { + private void freeSlot(int idx, boolean expireHandle) { ObjectSlot slot = slots[idx]; - setSlotObject(idx, null); + Object o = setSlotObjectImpl(idx, null, SetObjectActionType.DESTROY); + assert o == null; // must be already cleared when freeSlot is called slot.nextAtPos = slotsFreeListHead; slot.prevAtPos = -1; slot.posType = POS_TYPE_NONE; - slot.handle = 0; + if (slot.handle != null) { + if (expireHandle) { + objectScope.expireHandle(slot.handle); + } + slot.handle = null; + } slot.markedForDestroy = slot.markedForUpdate = false; slotsFreeListHead = idx; + invalidateQueryGeneration(); } private static final int POS_TYPE_NONE = 0; // not valid private static final int POS_TYPE_GLOBAL = 1; // always loaded if scene loaded private static final int POS_TYPE_BLOCK = 2; // exists at block pos - private static class ObjectSlot { - long handle = 0; + static class ObjectSlot { + GameObjectHandle handle; int x, y, z; int posType = POS_TYPE_NONE; Object object; @@ -448,12 +470,6 @@ ChunkAABB getChunkAABB() { } return null; } - - boolean isSerializationNeeded(SerializationContext ctx) { - if (object == null) - return false; - return ObjectSerializer.getSerializerTypeIdFromObject(object) != null; - } } private ObjectSlot[] slots = new ObjectSlot[0]; @@ -476,30 +492,25 @@ private void setSlotLoaded(int slotIdx, boolean isLoaded) { } } - private void setSlotObject(int slotIdx, Object object) { - ObjectSlot slot = slots[slotIdx]; - if (slot.object != object) { - setSlotLoaded(slotIdx, false); - if (slot.isObjectInitialized) { - ComponentUtils.safeCallDestroyFor(slot.object); - } - ComponentUtils.safeCallFinalizeObjectFor(slot.object); - slot.object = object; - slot.isObjectInitialized = false; - } + private enum SetObjectActionType { + UNLINK, + DISCARD, + DESTROY } - private Object discardSlotObject(int slotIdx) { + private Object setSlotObjectImpl(int slotIdx, Object object, SetObjectActionType action) { ObjectSlot slot = slots[slotIdx]; - Object o = null; - if (slot.object != null) { + Object o = slot.object; + if (slot.object != object) { setSlotLoaded(slotIdx, false); if (slot.isObjectInitialized) { - ComponentUtils.safeCallDiscardFor(slot.object); + if (action == SetObjectActionType.DISCARD) + ComponentUtils.safeCallDiscardFor(slot.object); + else if (action == SetObjectActionType.DESTROY) + ComponentUtils.safeCallDestroyFor(slot.object); } ComponentUtils.safeCallFinalizeObjectFor(slot.object); - o = slot.object; - slot.object = null; + slot.object = object; slot.isObjectInitialized = false; } return o; @@ -544,7 +555,6 @@ void remove(int idx) { } private HashMap chunks = new HashMap<>(); - private HashMap handleToSlotIdx = new HashMap<>(); private HashMap lookupByPos = new HashMap<>(); private void updateObjectBlockPos(int slotIdx, BlockPos prevPos, BlockPos newPos) { @@ -558,11 +568,12 @@ private void updateObjectBlockPos(int slotIdx, BlockPos prevPos, BlockPos newPos if (slot.prevAtPos != -1) slots[slot.prevAtPos].nextAtPos = slot.nextAtPos; if (slot.nextAtPos != -1) slots[slot.nextAtPos].prevAtPos = slot.prevAtPos; slot.prevAtPos = slot.nextAtPos = -1; + invalidateQueryGeneration(); } if (newPos != null) { Integer firstSlotIdx = lookupByPos.get(newPos); if (firstSlotIdx != null) { - ObjectSlot first = slots[slotIdx]; + ObjectSlot first = slots[firstSlotIdx]; slot.nextAtPos = first.nextAtPos; if (first.nextAtPos != -1) slots[first.nextAtPos].prevAtPos = slotIdx; first.nextAtPos = slotIdx; @@ -604,7 +615,7 @@ private void serializeOneObject(IScopedWriter scope, ObjectSlot slot, String typ IArrayWriter writer = scope.asArray(); writer.addInt(slot.posType); writer.addString(typeId); - writer.addLong(slot.handle); + writer.addLong(slot.handle.value); ObjectSerializer.serializeObjectUnpacked(writer.beginScope(), slot.object, typeId, ctx, true); switch (slot.posType) { case POS_TYPE_GLOBAL: @@ -617,6 +628,16 @@ private void serializeOneObject(IScopedWriter scope, ObjectSlot slot, String typ } private GameObjectHandle deserializeAndInitOneObject(IScopedReader scope, SerializationContext ctx, IArrayWriter unresolvedList) { + try { + return deserializeAndInitOneObjectImpl(scope, ctx, unresolvedList); + } catch (Throwable err) { + ctx.addError(err, null, "failed to deserialize game object"); + scope.forceEndScope(ctx); + return null; + } + } + + private GameObjectHandle deserializeAndInitOneObjectImpl(IScopedReader scope, SerializationContext ctx, IArrayWriter unresolvedList) { IArrayReader reader = scope.asArray(); if (reader == null || reader.size() < 4) { ctx.addError("unexpected data format for game object"); @@ -632,9 +653,8 @@ private GameObjectHandle deserializeAndInitOneObject(IScopedReader scope, Serial return null; } if (!ObjectSerializer.isTypeRegistered(typeId)) { - // no saver registered for this type, keep serialized object to avoid loosing data + // no saver registered for this type, keep serialized object to avoid losing data SerializationUtils.readerToWriter(scope, unresolvedList.beginScope()); - scope.endScope(); return null; } Object object = ObjectSerializer.deserializeObjectUnpacked(reader.beginScope(3), typeId, Object.class, ctx, true); @@ -646,11 +666,12 @@ private GameObjectHandle deserializeAndInitOneObject(IScopedReader scope, Serial GameObjectHandle handle = null; switch (posType) { case POS_TYPE_GLOBAL: - handle = create(object).placeAsGlobal(); + handle = createImpl(objectScope.getOrCreateHandle(rawHandle), object).placeAsGlobal(); break; case POS_TYPE_BLOCK: if (reader.size() < 7) { ctx.addError("failed to deserialize object block pos"); + scope.endScope(); return null; } int x = reader.getInt(4, Integer.MAX_VALUE); @@ -658,9 +679,10 @@ private GameObjectHandle deserializeAndInitOneObject(IScopedReader scope, Serial int z = reader.getInt(6, Integer.MAX_VALUE); if (x == Integer.MAX_VALUE || y == Integer.MAX_VALUE || z == Integer.MAX_VALUE) { ctx.addError("failed to deserialize object block pos"); + scope.endScope(); return null; } - handle = create(object).placeAtBlockPos(new BlockPos(x, y, z)); + handle = createImpl(objectScope.getOrCreateHandle(rawHandle), object).placeAtBlockPos(new BlockPos(x, y, z)); break; } if (handle != null) { @@ -763,7 +785,7 @@ private boolean updateAllMarkedObjects(BlockSource region) { for (int slotIdx : objectsMarkedForUpdate) { ObjectSlot slot = slots[slotIdx]; if (slot.markedForDestroy) { - destroySlotImpl(slotIdx); + unlinkSlotImpl(slotIdx, SetObjectActionType.DESTROY); continue; } if (slot.markedForUpdate) { @@ -809,8 +831,9 @@ public void runPeriodicChecks(int maxChecksAbs, float maxChecksRel, int maxTimeU int numChecks = Math.min(maxChecksAbs, Math.max(1, (int) (periodicChecksListSize * maxChecksRel))); if (numChecks <= 0 || periodicChecksListSize == 0) return; + long endTimeNs = maxTimeUs > 0 ? System.nanoTime() + maxTimeUs * 1000L : Long.MAX_VALUE; periodicChecksListNextIdx %= periodicChecksListSize; - for (int i = 0; i < numChecks; i++) { + for (int i = 0; i < numChecks && periodicChecksListSize > 0; i++) { int idx = periodicChecksListNextIdx; int slotIdx = periodicChecksList[idx]; ObjectSlot slot = slots[slotIdx]; @@ -823,6 +846,8 @@ public void runPeriodicChecks(int maxChecksAbs, float maxChecksRel, int maxTimeU } if (periodicChecksListNextIdx >= periodicChecksListSize) periodicChecksListNextIdx = 0; + if (System.nanoTime() >= endTimeNs) + break; } } @@ -836,12 +861,12 @@ public void runPeriodicChecks(int maxChecksAbs, float maxChecksRel, int maxTimeU public static GameObjectManager getForDimension(int dimensionId) { GameObjectManager scene = defaultMgrForDimension.get(dimensionId); if (scene == null) { - defaultMgrForDimension.put(dimensionId, scene = new GameObjectManager("default-" + dimensionId).setDimensionId(dimensionId)); + defaultMgrForDimension.put(dimensionId, scene = new GameObjectManager(GameObjectScope.SERVER_SCOPE, "default-" + dimensionId).setDimensionId(dimensionId).setDefaultScene(true)); } return scene; } private static final HashMap defaultMgrForDimension = new HashMap<>(); - private static final GameObjectManager defaultServerMgr = new GameObjectManager("default-server-global"); + private static final GameObjectManager defaultServerMgr = new GameObjectManager(GameObjectScope.SERVER_SCOPE, "default-server-global").setDefaultScene(true); @ApiBinding public static GameObjectManager getGlobalForServer() { return defaultServerMgr; } @@ -851,18 +876,18 @@ public static GameObjectManager getForDimension(int dimensionId) { // - not loaded on server @ApiBinding public static GameObjectManager getClientSide() { return clientSideScene; } - private static final GameObjectManager clientSideScene = new GameObjectManager("default-client"); + private static final GameObjectManager clientSideScene = new GameObjectManager(GameObjectScope.CLIENT_SCOPE, "default-client").setDefaultScene(true); - public static void clearAllDefaultServerScenes(boolean callDestroyForObjects) { + public static void clearAllDefaultServerScenes(boolean runLifeCycleCallbacks, boolean isDestroy) { for (GameObjectManager mgr : defaultMgrForDimension.values()) { - mgr.clearAllObjects(callDestroyForObjects); + mgr.clearAllObjects(runLifeCycleCallbacks, isDestroy); } defaultMgrForDimension.clear(); - defaultServerMgr.clearAllObjects(callDestroyForObjects); + defaultServerMgr.clearAllObjects(runLifeCycleCallbacks, isDestroy); } - public static void clearAllDefaultClientScenes(boolean callDestroyForObjects) { - clientSideScene.clearAllObjects(callDestroyForObjects); + public static void clearAllDefaultClientScenes(boolean runLifeCycleCallbacks, boolean isDestroy) { + clientSideScene.clearAllObjects(runLifeCycleCallbacks, isDestroy); } /* @@ -904,11 +929,15 @@ void serializeAllObjectsAndAddUnresolved(IScopedWriter scope, SerializationConte IArrayWriter writer = scope.asArray(); if (!unresolvedSavedObjectsList.isEmpty()) { - IArrayReader unresolvedObjectsList = unresolvedSavedObjectsList.beginRead().asArray(); - for (int i = 0; i < unresolvedObjectsList.size(); i++) { - SerializationUtils.readerToWriter(unresolvedObjectsList.beginScope(i), writer.beginScope()); + IScopedReader unresolvedScope = unresolvedSavedObjectsList.beginRead(); + try { + IArrayReader unresolvedObjectsList = unresolvedScope.asArray(); + for (int i = 0; i < unresolvedObjectsList.size(); i++) { + SerializationUtils.readerToWriter(unresolvedObjectsList.beginScope(i), writer.beginScope()); + } + } finally { + unresolvedScope.forceEndScope(ctx); } - unresolvedObjectsList.endScope(); } for (int i = 0; i < slotsSize; i++) { diff --git a/src/main/java/org/innercore/icstd/scene/GameObjectScope.java b/src/main/java/org/innercore/icstd/scene/GameObjectScope.java new file mode 100644 index 00000000..501dd754 --- /dev/null +++ b/src/main/java/org/innercore/icstd/scene/GameObjectScope.java @@ -0,0 +1,62 @@ +package org.innercore.icstd.scene; + +import org.innercore.icstd.annotations.ApiHidden; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class GameObjectScope { + private static final long HANDLE_VALUE_BITS = 48L; + public static final GameObjectScope SERVER_SCOPE = new GameObjectScope(true, 0L); + public static final GameObjectScope CLIENT_SCOPE = new GameObjectScope(false, 1L << HANDLE_VALUE_BITS); + + private final boolean isServerSide; + private final AtomicLong nextUniqueHandleValue = new AtomicLong(1); + + private GameObjectScope(boolean isServerSide, long uniqueHandleOffset) { + this.isServerSide = isServerSide; + // NOTE: this is implementation defined, any mechanism for generating unique long handles will be suitable + // reserve 48 bits for a range, 15 bits for offset + assert (uniqueHandleOffset & ((1L << HANDLE_VALUE_BITS) - 1L)) == 0L; + // seed handles with current millisecond time on init, time is monotonically increasing, assume mods won't generate more than 16 handles per millisecond on average + // this will last us 2^44 ms or > 500 years without repeating + // because its implementation details, this can be relaxed a lot, if some bits are needed for packing additional data, or just reworked entirely + nextUniqueHandleValue.set((((System.currentTimeMillis() << 4) + 1) & ((1L << HANDLE_VALUE_BITS) - 1L)) + uniqueHandleOffset); + } + + private final HashMap handles = new HashMap<>(); + + GameObjectHandle getOrCreateHandle(long value) { + return handles.computeIfAbsent(value, (Long _key) -> new GameObjectHandle(this, value)); + } + + GameObjectHandle createUniqueHandle() { + return getOrCreateHandle(nextUniqueHandleValue.getAndAdd(1)); + } + + void expireHandle(GameObjectHandle handle) { + if (handle.isExpired) + return; + if (handle.scope != this) + throw new IllegalArgumentException("handle of different scope"); + handle.scene = null; + handle.slotIdx = -1; + handle.isExpired = true; + handles.remove(handle.value); + } + + // NOTE: Only call after all scenes of this scope are cleared + @ApiHidden + public void expireAllHandles() { + for (GameObjectHandle handle : handles.values()) { + handle.scene = null; + handle.slotIdx = -1; + handle.isExpired = true; + } + handles.clear(); + } + + public boolean isServerSide() { + return isServerSide; + } +} diff --git a/src/main/java/org/innercore/icstd/serialization/ObjectSerializer.java b/src/main/java/org/innercore/icstd/serialization/ObjectSerializer.java index 55dade36..c6840f47 100644 --- a/src/main/java/org/innercore/icstd/serialization/ObjectSerializer.java +++ b/src/main/java/org/innercore/icstd/serialization/ObjectSerializer.java @@ -12,7 +12,7 @@ public class ObjectSerializer { private static final HashMap legacyHashIdLookup = new HashMap<>(); public static final String SKIP_SERIALIZATION_TYPE_ID = registerType("__serialization_skip", Object.class, new IObjectSerializer() { - @Override public Object read(IScopedReader reader, SerializationContext ctx, String typeId) { return null; } + @Override public Object read(IScopedReader reader, SerializationContext ctx, String typeId) { reader.endScope(); return null; } @Override public void write(IScopedWriter writer, Object object, SerializationContext ctx, String typeId) { writer.endScope(); } }, false); @@ -66,11 +66,6 @@ public static String getTypeIdFromLegacyHashId(int legacyHashId, String fallback return rec != null ? rec.typeId : fallback; } - public static String getTypeIdFromLegacyHashIdNotNull(int legacyHashId) { - String typeId = getTypeIdFromLegacyHashId(legacyHashId, null); - return typeId != null ? typeId : "__unknown_hash_id:" + legacyHashId; - } - public static int getLegacyHashIdFromTypeId(String typeId, int fallback) { SerializerRecord rec = getSerializerRecordByTypeId(typeId); return rec != null ? rec.legacyHashId : fallback; @@ -135,23 +130,26 @@ public void endScope() { } } - // NOTE: returns null and leaves reader untouched, if it is not a packed object + // NOTE: returns null and leaves reader untouched, if it is not a packed object. + // Objects packed with a type id that is not registered are also reported as not packed, so + // generic readers fall through to reading them as plain data - the raw payload (including the + // type marker) then survives load/save cycles while its saver mod is missing or disabled public static PackedObjectReader unpackObjectAndTypeId(IScopedReader reader, SerializationContext ctx) { IArrayReader array = reader.asArray(); String typeId = null; IScopedReader target = reader; if (array != null && array.size() == 2 && array.getValueType(0).isString() && array.getValueType(1).isScope()) { String str = array.getString(0, ""); - if (str.startsWith("_t:")) { + if (str.startsWith("_t:") && isTypeRegistered(str.substring(3))) { typeId = str.substring(3); target = array.beginScope(1); } } - if (ctx.hasLegacyFormat) { + if (typeId == null && ctx.hasLegacyFormat) { IObjectReader object = reader.asObject(); if (object != null && object.getValueType(ObjectSaverRegistry.PROPERTY_SAVER_ID).isNumber()) { int legacyTypeId = object.getInt(ObjectSaverRegistry.PROPERTY_SAVER_ID, 0); - typeId = getTypeIdFromLegacyHashIdNotNull(legacyTypeId); + typeId = getTypeIdFromLegacyHashId(legacyTypeId, null); } } if (typeId == null) { @@ -178,6 +176,8 @@ public static IScopedWriter packObjectAndTypeId(IScopedWriter writer, String typ } } + // NOTE: returns null both when the object has no saver mark at all and when its legacy hash id + // does not resolve to a registered type - callers must not assume a non-null result is registered public static String getSerializerTypeIdFromObject(Object src) { if (src instanceof ISerializableObject) { String typeId = ((ISerializableObject) src).getSerializationTypeId(); @@ -185,16 +185,28 @@ public static String getSerializerTypeIdFromObject(Object src) { return typeId; } if (src instanceof ObjectSaverRegistry.IHashSaver) { - int hashId = ((ObjectSaverRegistry.IHashSaver) src).getObjectSaverId(); - return getTypeIdFromLegacyHashIdNotNull(hashId); + try { + int hashId = ((ObjectSaverRegistry.IHashSaver) src).getObjectSaverId(); + return getTypeIdFromLegacyHashId(hashId, null); + } catch (Throwable err) { + ICLog.e("SAVER", "legacy saver id lookup failed for " + src.getClass(), err); + return null; + } } else if (src instanceof Scriptable) { Scriptable scriptable = (Scriptable) src; - Object saverId = scriptable.get(ObjectSaverRegistry.PROPERTY_SAVER_ID, scriptable); + Object saverId; + try { + // property access can run a script getter, it must not break serialization + saverId = scriptable.get(ObjectSaverRegistry.PROPERTY_SAVER_ID, scriptable); + } catch (Throwable err) { + ICLog.e("SAVER", "saver id property lookup failed for " + scriptable.getClass(), err); + saverId = Scriptable.NOT_FOUND; + } if (saverId != Scriptable.NOT_FOUND) { if (saverId instanceof CharSequence) { return saverId.toString(); } else if (saverId instanceof Number) { - return getTypeIdFromLegacyHashIdNotNull(((Number) saverId).intValue()); + return getTypeIdFromLegacyHashId(((Number) saverId).intValue(), null); } } } else if (src instanceof JSONObject) { @@ -202,7 +214,7 @@ public static String getSerializerTypeIdFromObject(Object src) { if (saverId instanceof String) { return (String) saverId; } else if (saverId instanceof Number) { - return getTypeIdFromLegacyHashIdNotNull(((Number) saverId).intValue()); + return getTypeIdFromLegacyHashId(((Number) saverId).intValue(), null); } } else if (src instanceof Wrapper) { return getSerializerTypeIdFromObject(((Wrapper) src).unwrap()); @@ -267,12 +279,17 @@ public static boolean serializeObject(IScopedWriter io, Object o, SerializationC return false; } IScopedWriter writer = packObjectAndTypeId(io, typeId, ctx); - boolean success; + boolean success = false; try { success = serializeObjectUnpacked(writer, o, typeId, ctx, optional); } finally { - if (writer != io) + if (writer != io && io.isInScope()) { + // break the packed shape of a failed object, so on load its partial payload is + // preserved as plain data instead of being reconstructed into garbage half-object + if (!success) + io.asArray().addAny("__corrupted"); io.endScope(); + } } return success; } @@ -298,7 +315,7 @@ public static T deserializeObjectUnpacked(IScopedReader io, String typeId, Cl else throw new IllegalArgumentException("deserializeObjectUnpacked(): " + msg); return null; } - if (!cls.isAssignableFrom(rec.objectType)) { + if (!cls.isAssignableFrom(rec.objectType) && !rec.objectType.isAssignableFrom(cls)) { io.endScope(); String msg = "read() of serializer " + rec.serializer.getClass() + " returns " + rec.objectType + " but expected " + cls; if (optional) ctx.addError(msg); diff --git a/src/main/java/org/innercore/icstd/serialization/ScriptableObjectSerializer.java b/src/main/java/org/innercore/icstd/serialization/ScriptableObjectSerializer.java index 71a0c7b0..bca83513 100644 --- a/src/main/java/org/innercore/icstd/serialization/ScriptableObjectSerializer.java +++ b/src/main/java/org/innercore/icstd/serialization/ScriptableObjectSerializer.java @@ -4,6 +4,7 @@ import com.zhekasmirnov.innercore.api.runtime.saver.ObjectSaverRegistry; import org.json.JSONObject; import org.mozilla.javascript.NativeArray; +import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; @@ -12,54 +13,77 @@ import java.util.List; public abstract class ScriptableObjectSerializer implements IObjectSerializer { - private static void writeImpl(IScopedWriter io, Object src, SerializationContext ctx) { + // silently convert number scriptable object keys to strings in serializer + public static final boolean ALLOW_NUMBER_KEYS_TO_STRING_CONVERSION = true; + + // js arrays are sparse and can have absurd length, writing them as dense json must not hang the save + public static final long MAX_SERIALIZED_ARRAY_LENGTH = 0x100000; + + private static void writeImpl(IScopedWriter io, Object src, SerializationContext ctx, boolean ignoreThisSaver) { if (src == null || src == Undefined.instance) { io.endScope(); // null scope return; } - if (src instanceof ScriptableObject && !ctx.visitedObjects.add(src)) { - ctx.addError(null, src, "cyclic reference found"); - io.endScope(); - return; + boolean addedToVisited = false; + if (src instanceof ScriptableObject) { + addedToVisited = ctx.visitedObjects.add(src); + if (!addedToVisited && !ignoreThisSaver) { + ctx.addError(null, src, "cyclic reference found"); + io.endScope(); + return; + } } - Object saverId = ObjectSerializer.getSerializerTypeIdFromObject(src); - if (saverId != null) { - ObjectSerializer.serializeObject(io, src, ctx, /* can fail */ true); - ctx.visitedObjects.remove(src); - return; + if (!ignoreThisSaver) { + String saverId = ObjectSerializer.getSerializerTypeIdFromObject(src); + if (saverId != null && ObjectSerializer.isTypeRegistered(saverId)) { + ObjectSerializer.serializeObject(io, src, ctx, /* can fail */ true); + if (addedToVisited) ctx.visitedObjects.remove(src); + return; + } } if (src instanceof NativeArray) { NativeArray array = (NativeArray) src; IArrayWriter writer = io.asArray(); - for (int i = 0; i < array.getLength(); i++) { + long length = array.getLength(); + if (length > MAX_SERIALIZED_ARRAY_LENGTH) { + ctx.addError(null, src, "array is too long to be serialized and will be truncated: length=" + length); + length = MAX_SERIALIZED_ARRAY_LENGTH; + } + for (long i = 0; i < length; i++) { Object o = array.get(i); if (ObjectSerializer.isPrimitiveType(o)) { writer.addAny(o); } else { - writeImpl(writer.beginScope(), o, ctx); + writeImpl(writer.beginScope(), o, ctx, false); } } io.endScope(); - ctx.visitedObjects.remove(src); + if (addedToVisited) ctx.visitedObjects.remove(src); return; } if (src instanceof ScriptableObject) { ScriptableObject scriptable = (ScriptableObject) src; IObjectWriter writer = io.asObject(); for (Object id : scriptable.getIds()) { + // value must be fetched with the original id: number ids are index slots, + // which a string lookup would miss + Object o = scriptable.get(id); if (!(id instanceof String)) { - ctx.addError(null, id, "serialized scriptable object can't have integer ids"); - continue; + if (ALLOW_NUMBER_KEYS_TO_STRING_CONVERSION) { + id = id + ""; + } else { + ctx.addError(null, id, "serialized scriptable object can't have number ids"); + continue; + } } - Object o = scriptable.get(id); if (ObjectSerializer.isPrimitiveType(o)) { writer.putAny((String) id, o); } else { - writeImpl(writer.beginScope((String) id), o, ctx); + writeImpl(writer.beginScope((String) id), o, ctx, false); } } io.endScope(); - ctx.visitedObjects.remove(src); + if (addedToVisited) ctx.visitedObjects.remove(src); return; } ctx.addError(null, src, "invalid object can't be serialized and will be skipped " + (src != null ? src.getClass() : null)); @@ -67,12 +91,12 @@ private static void writeImpl(IScopedWriter io, Object src, SerializationContext } public static void writeScriptable(IScopedWriter writer, Object object, SerializationContext ctx) { - writeImpl(writer, object, ctx); + writeImpl(writer, object, ctx, false); } @Override public void write(IScopedWriter writer, Object object, SerializationContext ctx, String typeId) { - writeImpl(writer, save(object), ctx); + writeImpl(writer, save(object), ctx, true); } private static Object readImpl(IScopedReader io, SerializationContext ctx, boolean ignoreThisSaver) { @@ -101,10 +125,19 @@ private static Object readImpl(IScopedReader io, SerializationContext ctx, boole IObjectReader reader = io.asObject(); ScriptableObject object = ScriptableObjectHelper.createEmpty(); for (String key : reader.iterKeys()) { + Object value; if (reader.getValueType(key).isScope()) { - object.put(key, object, readImpl(reader.beginScope(key), ctx, false)); + value = readImpl(reader.beginScope(key), ctx, false); } else { - object.put(key, object, reader.getAny(key, null)); + value = reader.getAny(key, null); + } + // number keys (written as strings by ALLOW_NUMBER_KEYS_TO_STRING_CONVERSION) must go + // back into index slots, a string put would make them invisible to js element access + long index = ScriptRuntime.indexFromString(key); + if (index != -1L) { + object.put((int) index, object, value); + } else { + object.put(key, object, value); } } io.endScope(); @@ -126,6 +159,10 @@ public static Object readScriptable(IScopedReader reader, SerializationContext c @Override public Object read(IScopedReader reader, SerializationContext ctx, String typeId) { Object result = readImpl(reader, ctx, true); + if (result == null) { + // nothing was saved (e.g. save() returned null), nothing to restore + return null; + } if (result instanceof ScriptableObject) { Object o = read((ScriptableObject) result); if (o instanceof Scriptable && ObjectSerializer.getSerializerTypeIdFromObject(o) == null) { diff --git a/src/main/java/org/innercore/internal/serialization/JsonIO.java b/src/main/java/org/innercore/internal/serialization/JsonIO.java index 9c2020e4..1c937e4a 100644 --- a/src/main/java/org/innercore/internal/serialization/JsonIO.java +++ b/src/main/java/org/innercore/internal/serialization/JsonIO.java @@ -152,7 +152,7 @@ private IArrayWriter setAnyImpl(int idx, Object value) { } @Override public IArrayWriter addInt(int value) { return addAnyImpl(value); } - @Override public IArrayWriter addInt(int idx, int value) { return addAnyImpl(value); } + @Override public IArrayWriter addInt(int idx, int value) { return addAnyImpl(idx, value); } @Override public IArrayWriter setInt(int idx, int value) { return setAnyImpl(idx, value); } @Override public IArrayWriter addLong(long value) { return addAnyImpl(value); } @Override public IArrayWriter addLong(int idx, long value) { return addAnyImpl(idx, value); } diff --git a/src/main/resources/innercore/coreengine/core-engine.dev.js b/src/main/resources/innercore/coreengine/core-engine.dev.js index fd02fc5b..bdbe80d9 100755 --- a/src/main/resources/innercore/coreengine/core-engine.dev.js +++ b/src/main/resources/innercore/coreengine/core-engine.dev.js @@ -594,7 +594,9 @@ var GameObjectRegistry = { read: function(obj) { var gameobject = gameObjectClass.instantiate(); for (var name in obj) { - gameobject[name] = data[name]; + if (typeof gameobject[name] != "function") { + gameobject[name] = obj[name]; + } } gameobject.isDeployed = false; gameobject.isInstance = true; From a6c975e583ffd6fc6136a4b6f36519aead62d8c0 Mon Sep 17 00:00:00 2001 From: reider745 Date: Fri, 17 Jul 2026 12:12:33 +0300 Subject: [PATCH 2/4] feat: support ServerChat --- .../com/reider745/event/EventListener.java | 5 +++++ .../innercore/api/NativeCallback.java | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/reider745/event/EventListener.java b/src/main/java/com/reider745/event/EventListener.java index 3ce6d239..868c6c96 100644 --- a/src/main/java/com/reider745/event/EventListener.java +++ b/src/main/java/com/reider745/event/EventListener.java @@ -464,6 +464,11 @@ public void onEntityTeleport(EntityTeleportEvent event) { NativeCallback.onCustomDimensionTransfer(EntityMethod.getIdForEntity(event.getEntity()), from, to); } + @EventHandler(priority = EventPriority.HIGHEST) + public void onChat(PlayerChatEvent event) { + consumeEvent(event, () -> NativeCallback.onServerChat(event.getPlayer().getNameTag(), event.getMessage(), EntityMethod.getIdForEntity(event.getPlayer()))); + } + @EventHandler(priority = EventPriority.HIGHEST) public void onServerCommand(ServerCommandEvent event) { final CommandSender sender = event.getSender(); diff --git a/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java b/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java index 12367cd3..7131677e 100644 --- a/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java +++ b/src/main/java/com/zhekasmirnov/innercore/api/NativeCallback.java @@ -543,7 +543,24 @@ public static void onExplode(float x, float y, float z, float power, long entity ); } - @ZoteOnly + @ClientOnly + public static void onLocalChatSent() { + String message = getStringParam("chat_message"); + Callback.invokeAPICallback("LocalChatSent", message); + } + + @ClientOnly + public static void onLocalChatReceived(boolean debounced) { + String author = getStringParam("local_chat_author"); + String message = getStringParam("local_chat_message"); + Callback.invokeAPICallback("LocalChatReceived", author, message, debounced); + } + + public static void onServerChat(String author, String message, long player) { + Callback.invokeAPICallback("ServerChat", author, message, player); + } + + public static void onServerCommand(String command, float x, float y, float z, long entity, boolean fromPacket) { Callback.invokeAPICallback("ServerCommand", command, new GenericScriptPos3(x, y, z), entity, fromPacket); } From 1cfe9373543977ab8595251438d82d07423a2a1c Mon Sep 17 00:00:00 2001 From: reider745 Date: Fri, 17 Jul 2026 12:12:52 +0300 Subject: [PATCH 3/4] fix: fix ItemContainerSlot.asScriptable() being not bound --- .../java/org/innercore/icstd/inventory/ItemContainerSlot.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/innercore/icstd/inventory/ItemContainerSlot.java b/src/main/java/org/innercore/icstd/inventory/ItemContainerSlot.java index 17a45310..937839ae 100644 --- a/src/main/java/org/innercore/icstd/inventory/ItemContainerSlot.java +++ b/src/main/java/org/innercore/icstd/inventory/ItemContainerSlot.java @@ -64,7 +64,7 @@ public class ItemContainerSlot extends ItemContainerSlotBindGenBase implements A return container; } - @ApiHidden public ScriptableObject asScriptable() { + @ApiBinding public ScriptableObject asScriptable() { ScriptableObject object = ScriptableObjectHelper.createEmpty(); object.put("id", object, id); object.put("count", object, count); From 1d613be2c271bbe65f42dd3bfa469a6c62f17605 Mon Sep 17 00:00:00 2001 From: reider745 Date: Fri, 17 Jul 2026 12:13:09 +0300 Subject: [PATCH 4/4] feat: merge changes modpack --- .../zhekasmirnov/horizon/util/FileUtils.java | 9 ++ .../innercore/modpack/ModPack.java | 4 + .../innercore/modpack/ModPackStorage.java | 32 +++++- .../DirectoryInstallationSource.java | 104 ++++++++++++++++++ .../ModpackInstallationSource.java | 1 + .../ZipFileInstallationSource.java | 15 ++- 6 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/zhekasmirnov/innercore/modpack/installation/DirectoryInstallationSource.java diff --git a/src/main/java/com/zhekasmirnov/horizon/util/FileUtils.java b/src/main/java/com/zhekasmirnov/horizon/util/FileUtils.java index 015cbe16..46fcb616 100644 --- a/src/main/java/com/zhekasmirnov/horizon/util/FileUtils.java +++ b/src/main/java/com/zhekasmirnov/horizon/util/FileUtils.java @@ -9,6 +9,7 @@ import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -155,4 +156,12 @@ public static void copyFileTree(File srcDir, File dstDir, Object dialog, String } } } + + public static String convertStreamToString(InputStream inputStream) { + try { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + return ""; + } + } } diff --git a/src/main/java/com/zhekasmirnov/innercore/modpack/ModPack.java b/src/main/java/com/zhekasmirnov/innercore/modpack/ModPack.java index ca2e4587..ff28a52c 100644 --- a/src/main/java/com/zhekasmirnov/innercore/modpack/ModPack.java +++ b/src/main/java/com/zhekasmirnov/innercore/modpack/ModPack.java @@ -67,6 +67,10 @@ public File getIconFile() { return new File(rootDirectory, "pack_icon.png"); } + public File getExternalServersFile() { + return new File(rootDirectory, "external_servers.txt"); + } + public ModPackManifest getManifest() { return manifest; } diff --git a/src/main/java/com/zhekasmirnov/innercore/modpack/ModPackStorage.java b/src/main/java/com/zhekasmirnov/innercore/modpack/ModPackStorage.java index 9f8d6b98..48f306f4 100644 --- a/src/main/java/com/zhekasmirnov/innercore/modpack/ModPackStorage.java +++ b/src/main/java/com/zhekasmirnov/innercore/modpack/ModPackStorage.java @@ -1,10 +1,7 @@ package com.zhekasmirnov.innercore.modpack; import com.zhekasmirnov.horizon.util.FileUtils; -import com.zhekasmirnov.innercore.modpack.installation.ExternalZipFileInstallationSource; -import com.zhekasmirnov.innercore.modpack.installation.ModpackInstallationSource; -import com.zhekasmirnov.innercore.modpack.installation.ZipFileExtractionTarget; -import com.zhekasmirnov.innercore.modpack.installation.ZipFileInstallationSource; +import com.zhekasmirnov.innercore.modpack.installation.*; import org.json.JSONException; import java.io.File; @@ -86,6 +83,33 @@ public boolean isDefaultModPack(ModPack modPack) { return defaultModPackDirectory.equals(modPack.getRootDirectory()); } + public ModPack getModPackByDirectory(File directory) { + if (defaultModPackDirectory.equals(directory)) { + return defaultModPack; + } + for (ModPack modPack : modPacks) { + if (modPack != null && modPack.getRootDirectory().equals(directory)) { + return modPack; + } + } + return null; + } + + public ModPack clonePack(ModPack modPack, ModPack.TaskReporter taskReporter) throws InterruptedException { + if (!modPack.reloadAndValidateManifest() || modPack.getManifest().getPackName() == null) { + ModPack.interruptTask("failed to load pack manifest"); + } + + try (DirectoryInstallationSource installationSource = new DirectoryInstallationSource(modPack)) { + return installNewModPack(installationSource, taskReporter); + } catch (IOException exception) { + taskReporter.reportError("failed to create installation source", exception, false); + ModPack.interruptTask(exception, "failed to create installation source"); + return null; + } + } + + private static String normalizeFileName(String name) { if (name == null || name.equals("")) { return "unnamed"; diff --git a/src/main/java/com/zhekasmirnov/innercore/modpack/installation/DirectoryInstallationSource.java b/src/main/java/com/zhekasmirnov/innercore/modpack/installation/DirectoryInstallationSource.java new file mode 100644 index 00000000..8c6c8c70 --- /dev/null +++ b/src/main/java/com/zhekasmirnov/innercore/modpack/installation/DirectoryInstallationSource.java @@ -0,0 +1,104 @@ +package com.zhekasmirnov.innercore.modpack.installation; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import com.zhekasmirnov.innercore.modpack.ModPack; +import com.zhekasmirnov.innercore.modpack.ModPackContext; +import com.zhekasmirnov.innercore.modpack.ModPackDirectory; +import com.zhekasmirnov.innercore.modpack.ModPackFactory; +import com.zhekasmirnov.innercore.modpack.ModPackStorage; +import com.zhekasmirnov.innercore.modpack.strategy.extract.DirectoryExtractStrategy; +import com.zhekasmirnov.innercore.utils.FileTools; + +public class DirectoryInstallationSource extends ModpackInstallationSource implements Closeable { + private final ModPack modpack; + private Collection extractEntries = null; + + public DirectoryInstallationSource(File directory) throws FileNotFoundException { + if (!directory.isDirectory()) { + throw new FileNotFoundException(directory.getPath() + " should be existing directory!"); + } + ModPackStorage storage = ModPackContext.getInstance().getStorage(); + ModPack modpack = storage.getModPackByDirectory(directory); + if (modpack == null) { + modpack = ModPackFactory.getInstance().createFromDirectory(directory); + } + this.modpack = modpack; + } + + public DirectoryInstallationSource(ModPack modpack) { + if (modpack == null) { + throw new NullPointerException("modpack == null"); + } + this.modpack = modpack; + } + + @Override + public String getManifestContent() throws IOException { + File manifestFile = modpack.getManifestFile(); + return FileTools.readFileText(manifestFile.getPath()); + } + + @Override + public String getExternalServersContent() throws IOException { + File externalServersFile = modpack.getExternalServersFile(); + return FileTools.readFileText(externalServersFile.getPath()); + } + + private Entry instantiateEntry(File file, DirectoryExtractStrategy extractStrategy) { + return new Entry() { + @Override + public String getName() { + return extractStrategy != null ? extractStrategy.getFullEntryName(file) : file.getName(); + } + + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream(file); + } + }; + } + + public synchronized Collection getExtractEntries() { + if (extractEntries == null) { + extractEntries = new ArrayList<>(); + for (ModPackDirectory directory : modpack.getAllDirectories()) { + DirectoryExtractStrategy extractStrategy = directory.getExtractStrategy(); + List filesToExtract = extractStrategy.getFilesToExtract(); + for (File file : filesToExtract) { + extractEntries.add(instantiateEntry(file, extractStrategy)); + } + } + File iconFile = modpack.getIconFile(); + if (iconFile.isFile()) { + extractEntries.add(instantiateEntry(iconFile, null)); + } + } + return extractEntries; + } + + @Override + public int getEntryCount() { + return getExtractEntries().size(); + } + + @Override + public Enumeration entries() { + return Collections.enumeration(getExtractEntries()); + } + + @Override + public void close() throws IOException { + extractEntries = null; + } +} diff --git a/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ModpackInstallationSource.java b/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ModpackInstallationSource.java index fdb0df86..621934fb 100644 --- a/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ModpackInstallationSource.java +++ b/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ModpackInstallationSource.java @@ -16,6 +16,7 @@ public interface Entry { } public abstract String getManifestContent() throws IOException; + public abstract String getExternalServersContent() throws IOException; public ModPackManifest getTempManifest() throws IOException, JSONException { ModPackManifest manifest = new ModPackManifest(); diff --git a/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ZipFileInstallationSource.java b/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ZipFileInstallationSource.java index 6983af9f..1ba8b01d 100644 --- a/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ZipFileInstallationSource.java +++ b/src/main/java/com/zhekasmirnov/innercore/modpack/installation/ZipFileInstallationSource.java @@ -1,5 +1,6 @@ package com.zhekasmirnov.innercore.modpack.installation; +import com.zhekasmirnov.horizon.util.FileUtils; import com.zhekasmirnov.innercore.utils.FileTools; import java.io.IOException; @@ -10,7 +11,7 @@ public class ZipFileInstallationSource extends ModpackInstallationSource { protected ZipFile file; - protected String manifestContent; + protected String manifestContent, externalServersContent; public ZipFileInstallationSource(ZipFile file) { setFile(file); @@ -39,6 +40,18 @@ public String getManifestContent() throws IOException { return manifestContent; } + @Override + public String getExternalServersContent() throws IOException { + if (externalServersContent == null) { + ZipEntry entry = file.getEntry("external_servers.txt"); + if (entry == null) { + throw new IOException("modpack zip file does not contain external_servers.txt"); + } + externalServersContent = FileUtils.convertStreamToString(file.getInputStream(entry)); + } + return externalServersContent; + } + @Override public int getEntryCount() { return file.size();