package emu.grasscutter.game.world; import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameDepot; import emu.grasscutter.data.excels.*; import emu.grasscutter.game.dungeons.DungeonChallenge; import emu.grasscutter.game.dungeons.DungeonSettleListener; import emu.grasscutter.game.entity.*; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.TeamInfo; import emu.grasscutter.game.props.ClimateType; import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.LifeState; import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; import emu.grasscutter.scripts.SceneScriptManager; import emu.grasscutter.scripts.data.SceneBlock; import emu.grasscutter.scripts.data.SceneGroup; import emu.grasscutter.server.packet.send.PacketAvatarSkillInfoNotify; import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify; import emu.grasscutter.server.packet.send.PacketSceneEntityDisappearNotify; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.danilopianini.util.SpatialIndex; import java.util.*; public class Scene { private final World world; private final SceneData sceneData; private final List players; private final Int2ObjectMap 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 DungeonChallenge challenge; private List dungeonSettleListeners; private DungeonData dungeonData; private int prevScene; // Id of the previous scene private int prevScenePoint; 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.time = 8 * 60; this.climate = ClimateType.CLIMATE_SUNNY; this.prevScene = 3; this.spawnedEntities = new HashSet<>(); this.deadSpawnedEntities = new HashSet<>(); this.loadedBlocks = new HashSet<>(); this.scriptManager = new SceneScriptManager(this); } public int getId() { return sceneData.getId(); } public World getWorld() { return world; } public SceneData getSceneData() { return this.sceneData; } public SceneType getSceneType() { return getSceneData().getSceneType(); } public List getPlayers() { return players; } public int getPlayerCount() { return this.getPlayers().size(); } public Int2ObjectMap getEntities() { return entities; } public GameEntity getEntityById(int id) { return this.entities.get(id); } public GameEntity getEntityByConfigId(int configId) { return this.entities.values().stream() .filter(x -> x.getConfigId() == configId) .findFirst() .orElse(null); } /** * @return the autoCloseTime */ public int getAutoCloseTime() { return autoCloseTime; } /** * @param autoCloseTime the autoCloseTime to set */ public void setAutoCloseTime(int autoCloseTime) { this.autoCloseTime = autoCloseTime; } public int getTime() { return time; } public void changeTime(int time) { this.time = time % 1440; } public ClimateType getClimate() { return climate; } public int getWeather() { return weather; } public void setClimate(ClimateType climate) { this.climate = climate; } public void setWeather(int weather) { this.weather = weather; } public int getPrevScene() { return prevScene; } public void setPrevScene(int prevScene) { this.prevScene = prevScene; } public int getPrevScenePoint() { return prevScenePoint; } public void setPrevScenePoint(int prevPoint) { this.prevScenePoint = prevPoint; } public boolean dontDestroyWhenEmpty() { return dontDestroyWhenEmpty; } public void setDontDestroyWhenEmpty(boolean dontDestroyWhenEmpty) { this.dontDestroyWhenEmpty = dontDestroyWhenEmpty; } public Set getLoadedBlocks() { return loadedBlocks; } public Set getSpawnedEntities() { return spawnedEntities; } public Set getDeadSpawnedEntities() { return deadSpawnedEntities; } public SceneScriptManager getScriptManager() { return scriptManager; } public DungeonData getDungeonData() { return dungeonData; } public void setDungeonData(DungeonData dungeonData) { if (dungeonData == null || this.dungeonData != null || this.getSceneType() != SceneType.SCENE_DUNGEON || dungeonData.getSceneId() != this.getId()) { return; } this.dungeonData = dungeonData; } public DungeonChallenge getChallenge() { return challenge; } public void setChallenge(DungeonChallenge challenge) { this.challenge = challenge; } public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener){ if(dungeonSettleListeners == null){ dungeonSettleListeners = new ArrayList<>(); } dungeonSettleListeners.add(dungeonSettleListener); } public List getDungeonSettleObservers() { return dungeonSettleListeners; } public boolean isInScene(GameEntity entity) { return this.entities.containsKey(entity.getId()); } public synchronized void addPlayer(Player player) { // Check if player already in if (getPlayers().contains(player)) { return; } // Remove player from prev scene if (player.getScene() != null) { player.getScene().removePlayer(player); } // Add getPlayers().add(player); player.setSceneId(this.getId()); player.setScene(this); this.setupPlayerAvatars(player); } public synchronized void removePlayer(Player player) { // Remove from challenge if leaving if (this.getChallenge() != null && this.getChallenge().inProgress()) { player.sendPacket(new PacketDungeonChallengeFinishNotify(this.getChallenge())); } // Remove player from scene getPlayers().remove(player); player.setScene(null); // Remove player avatars this.removePlayerAvatars(player); // Remove player gadgets for (EntityBaseGadget gadget : player.getTeamManager().getGadgets()) { this.removeEntity(gadget); } // Deregister scene if not in use if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty()) { this.getWorld().deregisterScene(this); } } private void setupPlayerAvatars(Player player) { // Clear entities from old team player.getTeamManager().getActiveTeam().clear(); // Add new entities for player TeamInfo teamInfo = player.getTeamManager().getCurrentTeamInfo(); for (int avatarId : teamInfo.getAvatars()) { EntityAvatar entity = new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatarId)); player.getTeamManager().getActiveTeam().add(entity); } // Limit character index in case its out of bounds if (player.getTeamManager().getCurrentCharacterIndex() >= player.getTeamManager().getActiveTeam().size() || player.getTeamManager().getCurrentCharacterIndex() < 0) { player.getTeamManager().setCurrentCharacterIndex(player.getTeamManager().getCurrentCharacterIndex() - 1); } } private void removePlayerAvatars(Player player) { Iterator it = player.getTeamManager().getActiveTeam().iterator(); while (it.hasNext()) { this.removeEntity(it.next(), VisionType.VISION_TYPE_REMOVE); it.remove(); } } public void spawnPlayer(Player player) { if (this.isInScene(player.getTeamManager().getCurrentAvatarEntity())) { return; } if (player.getTeamManager().getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { player.getTeamManager().getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); } this.addEntity(player.getTeamManager().getCurrentAvatarEntity()); // Notify the client of any extra skill charges for (EntityAvatar entity : player.getTeamManager().getActiveTeam()) { if (entity.getAvatar().getSkillExtraChargeMap().size() > 0) { player.sendPacket(new PacketAvatarSkillInfoNotify(entity.getAvatar())); } } } private void addEntityDirectly(GameEntity entity) { getEntities().put(entity.getId(), entity); } public synchronized void addEntity(GameEntity entity) { this.addEntityDirectly(entity); this.broadcastPacket(new PacketSceneEntityAppearNotify(entity)); } public synchronized void addEntityToSingleClient(Player player, GameEntity entity) { this.addEntityDirectly(entity); player.sendPacket(new PacketSceneEntityAppearNotify(entity)); } public synchronized void addEntities(Collection entities) { for (GameEntity entity : entities) { this.addEntityDirectly(entity); } this.broadcastPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_TYPE_BORN)); } private GameEntity removeEntityDirectly(GameEntity entity) { return getEntities().remove(entity.getId()); } public void removeEntity(GameEntity entity) { this.removeEntity(entity, VisionType.VISION_TYPE_DIE); } public synchronized void removeEntity(GameEntity entity, VisionType visionType) { GameEntity removed = this.removeEntityDirectly(entity); if (removed != null) { this.broadcastPacket(new PacketSceneEntityDisappearNotify(removed, visionType)); } } public synchronized void replaceEntity(EntityAvatar oldEntity, EntityAvatar newEntity) { this.removeEntityDirectly(oldEntity); this.addEntityDirectly(newEntity); this.broadcastPacket(new PacketSceneEntityDisappearNotify(oldEntity, VisionType.VISION_TYPE_REPLACE)); this.broadcastPacket(new PacketSceneEntityAppearNotify(newEntity, VisionType.VISION_TYPE_REPLACE, oldEntity.getId())); } public void showOtherEntities(Player player) { List entities = new LinkedList<>(); GameEntity currentEntity = player.getTeamManager().getCurrentAvatarEntity(); for (GameEntity entity : this.getEntities().values()) { if (entity == currentEntity) { continue; } entities.add(entity); } player.sendPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_TYPE_MEET)); } public void handleAttack(AttackResult result) { //GameEntity attacker = getEntityById(result.getAttackerId()); GameEntity target = getEntityById(result.getDefenseId()); if (target == null) { return; } // Godmode check if (target instanceof EntityAvatar) { if (((EntityAvatar) target).getPlayer().inGodmode()) { return; } } // Sanity check target.damage(result.getDamage(), result.getAttackerId()); } public void killEntity(GameEntity target, int attackerId) { GameEntity attacker = getEntityById(attackerId); //Check codex if (attacker instanceof EntityClientGadget) { var clientGadgetOwner = getEntityById(((EntityClientGadget) attacker).getOwnerEntityId()); if(clientGadgetOwner instanceof EntityAvatar) { ((EntityClientGadget) attacker).getOwner().getCodex().checkAnimal(target, CodexAnimalData.CodexAnimalUnlockCondition.CODEX_COUNT_TYPE_KILL); } } else if (attacker instanceof EntityAvatar) { ((EntityAvatar) attacker).getPlayer().getCodex().checkAnimal(target, CodexAnimalData.CodexAnimalUnlockCondition.CODEX_COUNT_TYPE_KILL); } // Packet this.broadcastPacket(new PacketLifeStateChangeNotify(attackerId, target, LifeState.LIFE_DEAD)); // Reward drop if (target instanceof EntityMonster && this.getSceneType() != SceneType.SCENE_DUNGEON) { getWorld().getServer().getDropManager().callDrop((EntityMonster) target); } this.removeEntity(target); // Death event target.onDeath(attackerId); } public void onTick() { if (this.getScriptManager().isInit()) { this.checkBlocks(); } else { // TEMPORARY this.checkSpawns(); } // Triggers this.getScriptManager().onTick(); } // TODO - Test public void checkSpawns() { SpatialIndex list = GameDepot.getSpawnListById(this.getId()); Set visible = new HashSet<>(); for (Player player : this.getPlayers()) { int RANGE = 100; Collection entries = list.query( new double[] {player.getPos().getX() - RANGE, player.getPos().getZ() - RANGE}, new double[] {player.getPos().getX() + RANGE, player.getPos().getZ() + RANGE} ); for (SpawnGroupEntry entry : entries) { for (SpawnDataEntry spawnData : entry.getSpawns()) { visible.add(spawnData); } } } // World level WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel()); int worldLevelOverride = 0; if (worldLevelData != null) { worldLevelOverride = worldLevelData.getMonsterLevel(); } // Todo List toAdd = new LinkedList<>(); List toRemove = new LinkedList<>(); for (SpawnDataEntry entry : visible) { if (!this.getSpawnedEntities().contains(entry) && !this.getDeadSpawnedEntities().contains(entry)) { // Spawn entity MonsterData data = GameData.getMonsterDataMap().get(entry.getMonsterId()); if (data == null) { continue; } int level = worldLevelOverride > 0 ? worldLevelOverride + entry.getLevel() - 22 : entry.getLevel(); level = level >= 100 ? 100 : level; level = level <= 0 ? 1 : level; EntityMonster entity = new EntityMonster(this, data, entry.getPos(), level); entity.getRotation().set(entry.getRot()); entity.setGroupId(entry.getGroup().getGroupId()); entity.setPoseId(entry.getPoseId()); entity.setConfigId(entry.getConfigId()); entity.setSpawnEntry(entry); toAdd.add(entity); // Add to spawned list this.getSpawnedEntities().add(entry); } } for (GameEntity entity : this.getEntities().values()) { if (entity.getSpawnEntry() != null && !visible.contains(entity.getSpawnEntry())) { toRemove.add(entity); } } if (toAdd.size() > 0) { toAdd.stream().forEach(this::addEntityDirectly); this.broadcastPacket(new PacketSceneEntityAppearNotify(toAdd, VisionType.VISION_TYPE_BORN)); } if (toRemove.size() > 0) { toRemove.stream().forEach(this::removeEntityDirectly); this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); } } public void checkBlocks() { Set visible = new HashSet<>(); for (Player player : this.getPlayers()) { for (SceneBlock block : getScriptManager().getBlocks()) { if (!block.contains(player.getPos())) { continue; } visible.add(block); } } Iterator it = this.getLoadedBlocks().iterator(); while (it.hasNext()) { SceneBlock block = it.next(); if (!visible.contains(block)) { it.remove(); onUnloadBlock(block); } } for (SceneBlock block : visible) { if (!this.getLoadedBlocks().contains(block)) { this.onLoadBlock(block); this.getLoadedBlocks().add(block); } } } // TODO optimize public void onLoadBlock(SceneBlock block) { for (SceneGroup group : block.groups) { // We load the script files for the groups here if (!group.isLoaded()) { this.getScriptManager().loadGroupFromScript(group); } group.triggers.forEach(getScriptManager()::registerTrigger); group.regions.forEach(getScriptManager()::registerRegion); } // Spawn gadgets AFTER triggers are added // TODO for (SceneGroup group : block.groups) { if (group.init_config == null) { continue; } int suite = group.init_config.suite; if (suite == 0) { continue; } do { this.getScriptManager().spawnGadgetsInGroup(group, suite); suite++; } while (suite < group.init_config.end_suite); } } public void onUnloadBlock(SceneBlock block) { List toRemove = this.getEntities().values().stream().filter(e -> e.getBlockId() == block.id).toList(); if (toRemove.size() > 0) { toRemove.stream().forEach(this::removeEntityDirectly); this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); } for (SceneGroup group : block.groups) { group.triggers.forEach(getScriptManager()::deregisterTrigger); group.regions.forEach(getScriptManager()::deregisterRegion); } } // Gadgets public void onPlayerCreateGadget(EntityClientGadget gadget) { // Directly add this.addEntityDirectly(gadget); // Add to owner's gadget list gadget.getOwner().getTeamManager().getGadgets().add(gadget); // Optimization if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { return; } this.broadcastPacketToOthers(gadget.getOwner(), new PacketSceneEntityAppearNotify(gadget)); } public void onPlayerDestroyGadget(int entityId) { GameEntity entity = getEntities().get(entityId); if (entity == null || !(entity instanceof EntityClientGadget)) { return; } // Get and remove entity EntityClientGadget gadget = (EntityClientGadget) entity; this.removeEntityDirectly(gadget); // Remove from owner's gadget list gadget.getOwner().getTeamManager().getGadgets().remove(gadget); // Optimization if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { return; } this.broadcastPacketToOthers(gadget.getOwner(), new PacketSceneEntityDisappearNotify(gadget, VisionType.VISION_TYPE_DIE)); } // Broadcasting public void broadcastPacket(BasePacket packet) { // Send to all players - might have to check if player has been sent data packets for (Player player : this.getPlayers()) { player.getSession().send(packet); } } public void broadcastPacketToOthers(Player excludedPlayer, BasePacket packet) { // Optimization if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == excludedPlayer) { return; } // Send to all players - might have to check if player has been sent data packets for (Player player : this.getPlayers()) { if (player == excludedPlayer) { continue; } // Send player.getSession().send(packet); } } }