diff --git a/src/main/java/emu/grasscutter/game/entity/EntityRegion.java b/src/main/java/emu/grasscutter/game/entity/EntityRegion.java new file mode 100644 index 000000000..5a09b7442 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/entity/EntityRegion.java @@ -0,0 +1,74 @@ +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass; +import emu.grasscutter.scripts.data.SceneRegion; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; +import lombok.Getter; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Getter +public class EntityRegion extends GameEntity{ + private final Position position; + private boolean hasNewEntities; + private final Set entities; // Ids of entities inside this region + private final SceneRegion metaRegion; + + public EntityRegion(Scene scene, SceneRegion region) { + super(scene); + this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION); + setGroupId(region.group.id); + setBlockId(region.group.block_id); + setConfigId(region.config_id); + this.position = region.pos.clone(); + this.entities = ConcurrentHashMap.newKeySet(); + this.metaRegion = region; + } + + public void addEntity(GameEntity entity) { + if (this.getEntities().contains(entity.getId())) { + return; + } + this.getEntities().add(entity.getId()); + this.hasNewEntities = true; + } + + public boolean hasNewEntities() { + return hasNewEntities; + } + + public void resetNewEntities() { + hasNewEntities = false; + } + + public void removeEntity(GameEntity entity) { + this.getEntities().remove(entity.getId()); + } + + @Override + public Int2FloatOpenHashMap getFightProperties() { + return null; + } + + @Override + public Position getPosition() { + return position; + } + + @Override + public Position getRotation() { + return null; + } + + @Override + public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { + /** + * The Region Entity would not be sent to client. + */ + return null; + } +} diff --git a/src/main/java/emu/grasscutter/game/props/EntityIdType.java b/src/main/java/emu/grasscutter/game/props/EntityIdType.java index 09414b65a..9ec6aee86 100644 --- a/src/main/java/emu/grasscutter/game/props/EntityIdType.java +++ b/src/main/java/emu/grasscutter/game/props/EntityIdType.java @@ -5,10 +5,11 @@ public enum EntityIdType { MONSTER (0x02), NPC (0x03), GADGET (0x04), - WEAPON (0x06), + REGION (0x05), + WEAPON (0x06), TEAM (0x09), MPLEVEL (0x0b); - + private final int id; private EntityIdType(int id) { diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 407edd938..1c40fe35e 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -30,24 +30,25 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.danilopianini.util.SpatialIndex; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; public class Scene { private final World world; private final SceneData sceneData; private final List players; - private final Int2ObjectMap entities; - + private final Map entities; private final Set spawnedEntities; private final Set deadSpawnedEntities; private final Set loadedBlocks; private boolean dontDestroyWhenEmpty; - + private int autoCloseTime; private int time; private ClimateType climate; private int weather; - + private SceneScriptManager scriptManager; private WorldChallenge challenge; private List dungeonSettleListeners; @@ -57,19 +58,19 @@ public class Scene { public Scene(World world, SceneData sceneData) { this.world = world; this.sceneData = sceneData; - this.players = Collections.synchronizedList(new ArrayList<>()); - this.entities = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); + this.players = new CopyOnWriteArrayList<>(); + this.entities = new ConcurrentHashMap<>(); this.time = 8 * 60; this.climate = ClimateType.CLIMATE_SUNNY; this.prevScene = 3; - - this.spawnedEntities = new HashSet<>(); - this.deadSpawnedEntities = new HashSet<>(); - this.loadedBlocks = new HashSet<>(); + + this.spawnedEntities = ConcurrentHashMap.newKeySet(); + this.deadSpawnedEntities = ConcurrentHashMap.newKeySet(); + this.loadedBlocks = ConcurrentHashMap.newKeySet(); this.scriptManager = new SceneScriptManager(this); } - + public int getId() { return sceneData.getId(); } @@ -89,15 +90,15 @@ public class Scene { public List getPlayers() { return players; } - + public int getPlayerCount() { return this.getPlayers().size(); } - public Int2ObjectMap getEntities() { + public Map getEntities() { return entities; } - + public GameEntity getEntityById(int id) { return this.entities.get(id); } @@ -629,15 +630,10 @@ public class Scene { var suiteData = group.getSuiteByIndex(suite); suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger); - entities.addAll(suiteData.sceneGadgets.stream() - .map(g -> scriptManager.createGadget(group.id, group.block_id, g)) - .filter(Objects::nonNull) - .toList()); - entities.addAll(suiteData.sceneMonsters.stream() - .map(mob -> scriptManager.createMonster(group.id, group.block_id, mob)) - .filter(Objects::nonNull) - .toList()); + entities.addAll(scriptManager.getGadgetsInGroupSuite(group, suiteData)); + entities.addAll(scriptManager.getMonstersInGroupSuite(group, suiteData)); + scriptManager.registerRegionInGroupSuite(group, suiteData); } scriptManager.meetEntities(entities); @@ -654,19 +650,18 @@ public class Scene { toRemove.forEach(this::removeEntityDirectly); this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); } - + for (SceneGroup group : block.groups.values()) { if(group.triggers != null){ group.triggers.values().forEach(getScriptManager()::deregisterTrigger); } if(group.regions != null){ - group.regions.forEach(getScriptManager()::deregisterRegion); + group.regions.values().forEach(getScriptManager()::deregisterRegion); } } scriptManager.getLoadedGroupSetPerBlock().remove(block.id); Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); } - // Gadgets public void onPlayerCreateGadget(EntityClientGadget gadget) { diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index 0c0012dc8..da2323e5c 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -6,10 +6,7 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.excels.MonsterData; import emu.grasscutter.data.excels.WorldLevelData; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.EntityNPC; -import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.entity.*; import emu.grasscutter.game.world.Scene; import emu.grasscutter.net.proto.VisionTypeOuterClass; import emu.grasscutter.scripts.constants.EventType; @@ -17,17 +14,12 @@ import emu.grasscutter.scripts.data.*; import emu.grasscutter.scripts.service.ScriptMonsterSpawnService; import emu.grasscutter.scripts.service.ScriptMonsterTideService; import io.netty.util.concurrent.FastThreadLocalThread; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.luaj.vm2.LuaError; import org.luaj.vm2.LuaValue; import org.luaj.vm2.lib.jse.CoerceJavaToLua; import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.stream.Collectors; public class SceneScriptManager { @@ -38,15 +30,15 @@ public class SceneScriptManager { /** * current triggers controlled by RefreshGroup */ - private final Int2ObjectOpenHashMap> currentTriggers; - private final Int2ObjectOpenHashMap regions; - private Map sceneGroups; + private final Map> currentTriggers; + private final Map regions; // + private final Map sceneGroups; private ScriptMonsterTideService scriptMonsterTideService; private ScriptMonsterSpawnService scriptMonsterSpawnService; /** * blockid - loaded groupSet */ - private Int2ObjectMap> loadedGroupSetPerBlock; + private final Map> loadedGroupSetPerBlock; public static final ExecutorService eventExecutor; static { eventExecutor = new ThreadPoolExecutor(4, 4, @@ -55,23 +47,23 @@ public class SceneScriptManager { } public SceneScriptManager(Scene scene) { this.scene = scene; - this.currentTriggers = new Int2ObjectOpenHashMap<>(); + this.currentTriggers = new ConcurrentHashMap<>(); - this.regions = new Int2ObjectOpenHashMap<>(); - this.variables = new HashMap<>(); - this.sceneGroups = new HashMap<>(); + this.regions = new ConcurrentHashMap<>(); + this.variables = new ConcurrentHashMap<>(); + this.sceneGroups = new ConcurrentHashMap<>(); this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this); - this.loadedGroupSetPerBlock = new Int2ObjectOpenHashMap<>(); + this.loadedGroupSetPerBlock = new ConcurrentHashMap<>(); // TEMPORARY if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) { return; } - + // Create this.init(); } - + public Scene getScene() { return scene; } @@ -123,19 +115,25 @@ public class SceneScriptManager { spawnMonstersInGroup(group, suite); spawnGadgetsInGroup(group, suite); } - public SceneRegion getRegionById(int id) { + public EntityRegion getRegionById(int id) { return regions.get(id); } - - public void registerRegion(SceneRegion region) { - regions.put(region.config_id, region); + + public void registerRegion(EntityRegion region) { + regions.put(region.getId(), region); } - - public void deregisterRegion(SceneRegion region) { - regions.remove(region.config_id); + public void registerRegionInGroupSuite(SceneGroup group, SceneSuite suite){ + suite.sceneRegions.stream().map(region -> new EntityRegion(this.getScene(), region)) + .forEach(this::registerRegion); + } + public synchronized void deregisterRegion(SceneRegion region) { + var instance = regions.values().stream() + .filter(r -> r.getConfigId() == region.config_id) + .findFirst(); + instance.ifPresent(entityRegion -> regions.remove(entityRegion.getId())); } - public Int2ObjectMap> getLoadedGroupSetPerBlock() { + public Map> getLoadedGroupSetPerBlock() { return loadedGroupSetPerBlock; } @@ -182,53 +180,61 @@ public class SceneScriptManager { } this.sceneGroups.put(group.id, group); - - if(group.regions != null){ - group.regions.forEach(this::registerRegion); - } } - + public void checkRegions() { if (this.regions.size() == 0) { return; } - - for (SceneRegion region : this.regions.values()) { + + for (var region : this.regions.values()) { getScene().getEntities().values() .stream() - .filter(e -> e.getEntityType() <= 2 && region.contains(e.getPosition())) + .filter(e -> e.getEntityType() <= 2 && region.getMetaRegion().contains(e.getPosition())) .forEach(region::addEntity); if (region.hasNewEntities()) { - // This is not how it works, source_eid should be region entity id, but we dont have an entity for regions yet - callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.config_id).setSourceEntityId(region.config_id)); - + callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.getConfigId()).setSourceEntityId(region.getId())); + region.resetNewEntities(); } } } + public List getGadgetsInGroupSuite(SceneGroup group, SceneSuite suite){ + return suite.sceneGadgets.stream() + .map(g -> createGadget(group.id, group.block_id, g)) + .filter(Objects::nonNull) + .toList(); + } + public List getMonstersInGroupSuite(SceneGroup group, SceneSuite suite){ + return suite.sceneMonsters.stream() + .map(mob -> createMonster(group.id, group.block_id, mob)) + .filter(Objects::nonNull) + .toList(); + } public void addGroupSuite(SceneGroup group, SceneSuite suite){ - spawnMonstersInGroup(group, suite); - spawnGadgetsInGroup(group, suite); - registerTrigger(suite.sceneTriggers); + // we added trigger first + registerTrigger(suite.sceneTriggers); + + var toCreate = new ArrayList(); + toCreate.addAll(getGadgetsInGroupSuite(group, suite)); + toCreate.addAll(getMonstersInGroupSuite(group, suite)); + addEntities(toCreate); + + registerRegionInGroupSuite(group, suite); } public void removeGroupSuite(SceneGroup group, SceneSuite suite){ + deregisterTrigger(suite.sceneTriggers); removeMonstersInGroup(group, suite); removeGadgetsInGroup(group, suite); - deregisterTrigger(suite.sceneTriggers); + + suite.sceneRegions.forEach(this::deregisterRegion); } - public void spawnGadgetsInGroup(SceneGroup group, int suiteIndex) { - spawnGadgetsInGroup(group, group.getSuiteByIndex(suiteIndex)); - } - - public void spawnGadgetsInGroup(SceneGroup group) { - spawnGadgetsInGroup(group, null); - } - + public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) { var gadgets = group.gadgets.values(); - + if (suite != null) { gadgets = suite.sceneGadgets; } @@ -240,13 +246,6 @@ public class SceneScriptManager { this.addEntities(toCreate); } - public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) { - var suite = group.getSuiteByIndex(suiteIndex); - if(suite == null){ - return; - } - spawnMonstersInGroup(group, suite); - } public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) { if(suite == null || suite.sceneMonsters.size() <= 0){ return; @@ -254,11 +253,6 @@ public class SceneScriptManager { this.addEntities(suite.sceneMonsters.stream() .map(mob -> createMonster(group.id, group.block_id, mob)).toList()); } - - public void spawnMonstersInGroup(SceneGroup group) { - this.addEntities(group.monsters.values().stream() - .map(mob -> createMonster(group.id, group.block_id, mob)).toList()); - } public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) { this.scriptMonsterTideService = diff --git a/src/main/java/emu/grasscutter/scripts/ScriptLib.java b/src/main/java/emu/grasscutter/scripts/ScriptLib.java index 631250c90..494282299 100644 --- a/src/main/java/emu/grasscutter/scripts/ScriptLib.java +++ b/src/main/java/emu/grasscutter/scripts/ScriptLib.java @@ -309,22 +309,22 @@ public class ScriptLib { return 0; } - + public int GetRegionEntityCount(LuaTable table) { logger.debug("[LUA] Call GetRegionEntityCount with {}", printTable(table)); int regionId = table.get("region_eid").toint(); int entityType = table.get("entity_type").toint(); - SceneRegion region = this.getSceneScriptManager().getRegionById(regionId); - + var region = this.getSceneScriptManager().getRegionById(regionId); + if (region == null) { return 0; } - return (int) region.getEntities().intStream().filter(e -> e >> 24 == entityType).count(); + return (int) region.getEntities().stream().filter(e -> e >> 24 == entityType).count(); } - + public void PrintContextLog(String msg) { logger.info("[LUA] " + msg); } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java index e670ddcb5..4455e78dd 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java @@ -34,10 +34,10 @@ public class SceneGroup { public Map gadgets; // public Map triggers; public Map npc; // - public List regions; + public Map regions; public List suites; public List variables; - + public SceneBusiness business; public SceneGarbage garbages; public SceneInitConfig init_config; @@ -115,9 +115,12 @@ public class SceneGroup { triggers.values().forEach(t -> t.currentGroup = this); suites = ScriptLoader.getSerializer().toList(SceneSuite.class, bindings.get("suites")); - regions = ScriptLoader.getSerializer().toList(SceneRegion.class, bindings.get("regions")); + regions = ScriptLoader.getSerializer().toList(SceneRegion.class, bindings.get("regions")).stream() + .collect(Collectors.toMap(x -> x.config_id, y -> y)); + regions.values().forEach(m -> m.group = this); + init_config = ScriptLoader.getSerializer().toObject(SceneInitConfig.class, bindings.get("init_config")); - + // Garbages TODO fix properly later Object garbagesValue = bindings.get("garbages"); if (garbagesValue != null && garbagesValue instanceof LuaValue garbagesTable) { @@ -157,12 +160,19 @@ public class SceneGroup { .map(triggers::get) .toList() ); + + suite.sceneRegions = new ArrayList<>( + suite.regions.stream() + .filter(regions::containsKey) + .map(regions::get) + .toList() + ); } } catch (ScriptException e) { Grasscutter.getLogger().error("Error loading group " + id + " in scene " + sceneId, e); } - + Grasscutter.getLogger().info("group {} in scene {} is loaded successfully.", id, sceneId); return this; } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneRegion.java b/src/main/java/emu/grasscutter/scripts/data/SceneRegion.java index edc04be15..ecba125eb 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneRegion.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneRegion.java @@ -1,62 +1,34 @@ package emu.grasscutter.scripts.data; -import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.scripts.constants.ScriptRegionShape; import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import lombok.Data; import lombok.Setter; -import lombok.ToString; -@ToString + @Setter public class SceneRegion { public int config_id; public int shape; public Position pos; + // for CUBIC public Position size; - - private boolean hasNewEntities; - private final IntSet entities; // Ids of entities inside this region - - public SceneRegion() { - this.entities = new IntOpenHashSet(); - } - - public IntSet getEntities() { - return entities; - } + // for SPHERE + public int radius; - public void addEntity(GameEntity entity) { - if (this.getEntities().contains(entity.getId())) { - return; - } - this.getEntities().add(entity.getId()); - this.hasNewEntities = true; - } - - public void removeEntity(GameEntity entity) { - this.getEntities().remove(entity.getId()); - } - - public boolean contains(Position p) { + public transient SceneGroup group; + public boolean contains(Position position) { switch (shape) { case ScriptRegionShape.CUBIC: - return (Math.abs(pos.getX() - p.getX()) <= size.getX()) && - (Math.abs(pos.getZ() - p.getZ()) <= size.getZ()); + return (Math.abs(pos.getX() - position.getX()) <= size.getX()) && + (Math.abs(pos.getY() - position.getY()) <= size.getY()) && + (Math.abs(pos.getZ() - position.getZ()) <= size.getZ()); case ScriptRegionShape.SPHERE: - return false; + var x = Math.pow(pos.getX() - position.getX(), 2); + var y = Math.pow(pos.getY() - position.getY(), 2); + var z = Math.pow(pos.getZ() - position.getZ(), 2); + return x + y + z <= (radius ^ 2); } - return false; } - public boolean hasNewEntities() { - return hasNewEntities; - } - - public void resetNewEntities() { - hasNewEntities = false; - } } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java b/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java index 7555601ca..6d5f62020 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneSuite.java @@ -11,9 +11,12 @@ public class SceneSuite { public List monsters; public List gadgets; public List triggers; - public int rand_weight; - + public List regions; + public int rand_weight; + public transient List sceneMonsters; public transient List sceneGadgets; public transient List sceneTriggers; + public transient List sceneRegions; + }