From e1770b5a682ce25aa6dd9bdb63b76af1afaf2d68 Mon Sep 17 00:00:00 2001 From: Akka <104902222+Akka0@users.noreply.github.com> Date: Wed, 25 May 2022 10:44:46 +0800 Subject: [PATCH] Support spawn NPC --- proto/GroupSuiteNotify.proto | 15 ++++ .../java/emu/grasscutter/data/GameData.java | 13 ++- .../emu/grasscutter/data/ResourceLoader.java | 36 +++++++-- .../data/custom/SceneNpcBornData.java | 28 +++++++ .../data/custom/SceneNpcBornEntry.java | 19 +++++ .../grasscutter/game/entity/EntityNPC.java | 81 +++++++++++++++++++ .../emu/grasscutter/game/world/Scene.java | 62 ++++++++++++-- .../scripts/SceneIndexManager.java | 3 +- .../scripts/SceneScriptManager.java | 20 +++-- .../grasscutter/scripts/data/SceneBlock.java | 12 ++- .../grasscutter/scripts/data/SceneGroup.java | 11 ++- .../grasscutter/scripts/data/SceneNPC.java | 10 +++ .../packet/send/PacketGroupSuiteNotify.java | 25 ++++++ 13 files changed, 303 insertions(+), 32 deletions(-) create mode 100644 proto/GroupSuiteNotify.proto create mode 100644 src/main/java/emu/grasscutter/data/custom/SceneNpcBornData.java create mode 100644 src/main/java/emu/grasscutter/data/custom/SceneNpcBornEntry.java create mode 100644 src/main/java/emu/grasscutter/game/entity/EntityNPC.java create mode 100644 src/main/java/emu/grasscutter/scripts/data/SceneNPC.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketGroupSuiteNotify.java diff --git a/proto/GroupSuiteNotify.proto b/proto/GroupSuiteNotify.proto new file mode 100644 index 000000000..884be64b6 --- /dev/null +++ b/proto/GroupSuiteNotify.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message GroupSuiteNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 3098; + } + + map group_map = 1; +} diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index e23bb0f32..21246c25a 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -7,12 +7,8 @@ import java.util.List; import java.util.Map; import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.custom.*; import emu.grasscutter.utils.Utils; -import emu.grasscutter.data.custom.AbilityEmbryoEntry; -import emu.grasscutter.data.custom.AbilityModifierEntry; -import emu.grasscutter.data.custom.OpenConfigEntry; -import emu.grasscutter.data.custom.MainQuestData; -import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.data.def.*; import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -28,7 +24,8 @@ public class GameData { private static final Map openConfigEntries = new HashMap<>(); private static final Map scenePointEntries = new HashMap<>(); private static final Int2ObjectMap mainQuestData = new Int2ObjectOpenHashMap<>(); - + private static final Int2ObjectMap npcBornData = new Int2ObjectOpenHashMap<>(); + // ExcelConfigs private static final Int2ObjectMap playerLevelDataMap = new Int2ObjectOpenHashMap<>(); @@ -131,7 +128,9 @@ public class GameData { public static Int2ObjectMap getMainQuestDataMap() { return mainQuestData; } - + public static Int2ObjectMap getSceneNpcBornData() { + return npcBornData; + } public static Int2ObjectMap getAvatarDataMap() { return avatarDataMap; } diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java index 6fe9b19fb..9d15fc61b 100644 --- a/src/main/java/emu/grasscutter/data/ResourceLoader.java +++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java @@ -1,13 +1,19 @@ package emu.grasscutter.data; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; +import ch.ethz.globis.phtree.PhTree; +import ch.ethz.globis.phtree.v16.PhTree16; import com.google.gson.Gson; +import emu.grasscutter.data.custom.*; import emu.grasscutter.utils.Utils; +import lombok.SneakyThrows; import org.reflections.Reflections; import com.google.gson.JsonElement; @@ -16,15 +22,9 @@ import com.google.gson.reflect.TypeToken; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.common.PointData; import emu.grasscutter.data.common.ScenePointConfig; -import emu.grasscutter.data.custom.AbilityEmbryoEntry; -import emu.grasscutter.data.custom.AbilityModifier; import emu.grasscutter.data.custom.AbilityModifier.AbilityConfigData; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.custom.AbilityModifier.AbilityModifierActionType; -import emu.grasscutter.data.custom.AbilityModifierEntry; -import emu.grasscutter.data.custom.OpenConfigEntry; -import emu.grasscutter.data.custom.MainQuestData; -import emu.grasscutter.data.custom.ScenePointEntry; import emu.grasscutter.game.world.SpawnDataEntry.*; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -65,6 +65,7 @@ public class ResourceLoader { loadQuests(); // Load scene points - must be done AFTER resources are loaded loadScenePoints(); + loadNpcBornData(); // Custom - TODO move this somewhere else try { GameData.getAvatarSkillDepotDataMap().get(504).setAbilities( @@ -418,6 +419,29 @@ public class ResourceLoader { Grasscutter.getLogger().info("Loaded " + GameData.getMainQuestDataMap().size() + " MainQuestDatas."); } + @SneakyThrows + private static void loadNpcBornData(){ + var folder = Files.list(Path.of(RESOURCE("BinOutput/Scene/SceneNpcBorn"))).toList(); + + for(var file : folder){ + if(file.toFile().isDirectory()){ + continue; + } + + PhTree index = new PhTree16<>(3); + + var data = Grasscutter.getGsonFactory().fromJson(Files.readString(file), SceneNpcBornData.class); + if(data.getBornPosList() == null || data.getBornPosList().size() == 0){ + continue; + } + data.getBornPosList().forEach(item -> index.put(item.getPos().toLongArray(), item)); + + data.setIndex(index); + GameData.getSceneNpcBornData().put(data.getSceneId(), data); + } + + Grasscutter.getLogger().info("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas."); + } // BinOutput configs private static class AvatarConfig { diff --git a/src/main/java/emu/grasscutter/data/custom/SceneNpcBornData.java b/src/main/java/emu/grasscutter/data/custom/SceneNpcBornData.java new file mode 100644 index 000000000..7aab358f2 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/SceneNpcBornData.java @@ -0,0 +1,28 @@ +package emu.grasscutter.data.custom; + +import ch.ethz.globis.phtree.PhTree; +import emu.grasscutter.scripts.data.SceneGroup; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class SceneNpcBornData { + int sceneId; + List bornPosList; + + /** + * Spatial Index For NPC + */ + transient PhTree index; + + /** + * npc groups + */ + transient Map groups = new ConcurrentHashMap<>(); +} diff --git a/src/main/java/emu/grasscutter/data/custom/SceneNpcBornEntry.java b/src/main/java/emu/grasscutter/data/custom/SceneNpcBornEntry.java new file mode 100644 index 000000000..1810c94bf --- /dev/null +++ b/src/main/java/emu/grasscutter/data/custom/SceneNpcBornEntry.java @@ -0,0 +1,19 @@ +package emu.grasscutter.data.custom; + +import emu.grasscutter.utils.Position; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class SceneNpcBornEntry { + int id; + int configId; + Position pos; + Position rot; + int groupId; + List suiteIdList; +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityNPC.java b/src/main/java/emu/grasscutter/game/entity/EntityNPC.java new file mode 100644 index 000000000..213951ed3 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/entity/EntityNPC.java @@ -0,0 +1,81 @@ +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.*; +import emu.grasscutter.scripts.data.SceneNPC; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; + +public class EntityNPC extends GameEntity{ + + private final Position position; + private final Position rotation; + private final SceneNPC metaNpc; + private final int suiteId; + + public EntityNPC(Scene scene, SceneNPC metaNPC, int blockId, int suiteId) { + super(scene); + this.id = getScene().getWorld().getNextEntityId(EntityIdType.NPC); + setConfigId(metaNPC.config_id); + setGroupId(metaNPC.group.id); + setBlockId(blockId); + this.suiteId = suiteId; + this.position = metaNPC.pos.clone(); + this.rotation = metaNPC.rot.clone(); + this.metaNpc = metaNPC; + + } + + @Override + public Int2FloatOpenHashMap getFightProperties() { + return null; + } + + @Override + public Position getPosition() { + return position; + } + + @Override + public Position getRotation() { + return rotation; + } + + public int getSuiteId() { + return suiteId; + } + + @Override + public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { + + EntityAuthorityInfoOuterClass.EntityAuthorityInfo authority = EntityAuthorityInfoOuterClass.EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo(EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo.newBuilder()) + .setAiInfo(SceneEntityAiInfoOuterClass.SceneEntityAiInfo.newBuilder() + .setIsAiOpen(true) + .setBornPos(getPosition().toProto())) + .setBornPos(getPosition().toProto()) + .build(); + + SceneEntityInfoOuterClass.SceneEntityInfo.Builder entityInfo = SceneEntityInfoOuterClass.SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityTypeOuterClass.ProtEntityType.PROT_ENTITY_NPC) + .setMotionInfo(MotionInfoOuterClass.MotionInfo.newBuilder() + .setPos(getPosition().toProto()) + .setRot(getRotation().toProto()) + .setSpeed(VectorOuterClass.Vector.newBuilder())) + .addAnimatorParaList(AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair.newBuilder()) + .setEntityClientData(EntityClientDataOuterClass.EntityClientData.newBuilder()) + .setEntityAuthorityInfo(authority) + .setLifeState(1); + + + entityInfo.setNpc(SceneNpcInfoOuterClass.SceneNpcInfo.newBuilder() + .setNpcId(metaNpc.npc_id) + .setBlockId(getBlockId()) + .build()); + + return entityInfo.build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 3d5c85a00..44c10b10c 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -376,7 +376,7 @@ public class Scene { } entities.add(entity); } - + player.sendPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_MEET)); } @@ -535,6 +535,9 @@ public class Scene { .toList(); onLoadGroup(toLoad); } + for (Player player : this.getPlayers()) { + getScriptManager().meetEntities(loadNpcForPlayer(player, block)); + } } } @@ -590,7 +593,9 @@ public class Scene { List garbageGadgets = group.getGarbageGadgets(); if (garbageGadgets != null) { - garbageGadgets.forEach(g -> scriptManager.createGadget(group.id, group.block_id, g)); + entities.addAll(garbageGadgets.stream().map(g -> scriptManager.createGadget(group.id, group.block_id, g)) + .filter(Objects::nonNull) + .toList()); } // Load suites @@ -605,9 +610,13 @@ public class Scene { suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger); entities.addAll(suiteData.sceneGadgets.stream() - .map(g -> scriptManager.createGadget(group.id, group.block_id, g)).toList()); + .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)).toList()); + .map(mob -> scriptManager.createMonster(group.id, group.block_id, mob)) + .filter(Objects::nonNull) + .toList()); } @@ -626,7 +635,7 @@ public class Scene { this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_REMOVE)); } - for (SceneGroup group : block.groups) { + for (SceneGroup group : block.groups.values()) { if(group.triggers != null){ group.triggers.values().forEach(getScriptManager()::deregisterTrigger); } @@ -718,4 +727,47 @@ public class Scene { addEntity(entity); } } + public List loadNpcForPlayer(Player player, SceneBlock block){ + if(!block.contains(player.getPos())){ + return List.of(); + } + + int RANGE = 100; + var pos = player.getPos(); + var data = GameData.getSceneNpcBornData().get(getId()); + if(data == null){ + return List.of(); + } + + var npcs = SceneIndexManager.queryNeighbors(data.getIndex(), pos.toLongArray(), RANGE); + var entityNPCS = npcs.stream().map(item -> { + var group = data.getGroups().get(item.getGroupId()); + if(group == null){ + group = SceneGroup.of(item.getGroupId()); + data.getGroups().putIfAbsent(item.getGroupId(), group); + group.load(getId()); + } + + if(group.npc == null){ + return null; + } + var npc = group.npc.get(item.getConfigId()); + if(npc == null){ + return null; + } + + return getScriptManager().createNPC(npc, block.id, item.getSuiteIdList().get(0)); + }) + .filter(Objects::nonNull) + .filter(item -> getEntities().values().stream() + .filter(e -> e instanceof EntityNPC) + .noneMatch(e -> e.getConfigId() == item.getConfigId())) + .toList(); + + if(entityNPCS.size() > 0){ + broadcastPacket(new PacketGroupSuiteNotify(entityNPCS)); + } + + return entityNPCS; + } } diff --git a/src/main/java/emu/grasscutter/scripts/SceneIndexManager.java b/src/main/java/emu/grasscutter/scripts/SceneIndexManager.java index 3b48ed279..504884427 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneIndexManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneIndexManager.java @@ -4,12 +4,13 @@ import ch.ethz.globis.phtree.PhTree; import emu.grasscutter.utils.Position; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.function.Function; public class SceneIndexManager { - public static void buildIndex(PhTree tree, List elements, Function extractor){ + public static void buildIndex(PhTree tree, Collection elements, Function extractor){ elements.forEach(e -> tree.put(extractor.apply(e), e)); } public static List queryNeighbors(PhTree tree, Position position, int range){ diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index ef802dde9..9fd329838 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -7,6 +7,7 @@ import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.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.world.Scene; import emu.grasscutter.net.proto.VisionTypeOuterClass; @@ -140,14 +141,15 @@ public class SceneScriptManager { // TODO optimize public SceneGroup getGroupById(int groupId) { for (SceneBlock block : this.getScene().getLoadedBlocks()) { - for (SceneGroup group : block.groups) { - if (group.id == groupId) { - if(!group.isLoaded()){ - getScene().onLoadGroup(List.of(group)); - } - return group; - } + var group = block.groups.get(groupId); + if(group == null){ + continue; } + + if(!group.isLoaded()){ + getScene().onLoadGroup(List.of(group)); + } + return group; } return null; } @@ -365,7 +367,9 @@ public class SceneScriptManager { return entity; } - + public EntityNPC createNPC(SceneNPC npc, int blockId, int suiteId) { + return new EntityNPC(getScene(), npc, blockId, suiteId); + } public EntityMonster createMonster(int groupId, int blockId, SceneMonster monster) { if(monster == null){ return null; diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java b/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java index 0f4400a50..b42852d8f 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneBlock.java @@ -13,6 +13,8 @@ import javax.script.Bindings; import javax.script.CompiledScript; import javax.script.ScriptException; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static emu.grasscutter.Configuration.SCRIPT; @@ -24,7 +26,7 @@ public class SceneBlock { public Position min; public int sceneId; - public List groups; + public Map groups; public PhTree sceneGroupIndex = new PhTree16<>(3); private transient boolean loaded; // Not an actual variable in the scripts either @@ -61,9 +63,11 @@ public class SceneBlock { cs.eval(bindings); // Set groups - groups = ScriptLoader.getSerializer().toList(SceneGroup.class, bindings.get("groups")); - groups.forEach(g -> g.block_id = id); - SceneIndexManager.buildIndex(this.sceneGroupIndex, groups, g -> g.pos.toLongArray()); + groups = ScriptLoader.getSerializer().toList(SceneGroup.class, bindings.get("groups")).stream() + .collect(Collectors.toMap(x -> x.id, y -> y)); + + groups.values().forEach(g -> g.block_id = id); + SceneIndexManager.buildIndex(this.sceneGroupIndex, groups.values(), g -> g.pos.toLongArray()); } catch (ScriptException e) { Grasscutter.getLogger().error("Error loading block " + id + " in scene " + sceneId, e); } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java index 9275706a4..0be30342d 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGroup.java @@ -32,7 +32,7 @@ public class SceneGroup { public Map monsters; // public Map gadgets; // public Map triggers; - + public Map npc; // public List regions; public List suites; public List variables; @@ -44,6 +44,11 @@ public class SceneGroup { private transient boolean loaded; // Not an actual variable in the scripts either private transient CompiledScript script; private transient Bindings bindings; + public static SceneGroup of(int groupId) { + var group = new SceneGroup(); + group.id = groupId; + return group; + } public boolean isLoaded() { return loaded; @@ -124,6 +129,10 @@ public class SceneGroup { // Add variables to suite variables = ScriptLoader.getSerializer().toList(SceneVar.class, bindings.get("variables")); + // NPC in groups + npc = ScriptLoader.getSerializer().toList(SceneNPC.class, bindings.get("npcs")).stream() + .collect(Collectors.toMap(x -> x.npc_id, y -> y)); + npc.values().forEach(n -> n.group = this); // Add monsters and gadgets to suite for (SceneSuite suite : suites) { diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneNPC.java b/src/main/java/emu/grasscutter/scripts/data/SceneNPC.java new file mode 100644 index 000000000..97f3f79dd --- /dev/null +++ b/src/main/java/emu/grasscutter/scripts/data/SceneNPC.java @@ -0,0 +1,10 @@ +package emu.grasscutter.scripts.data; + +import lombok.Setter; +import lombok.ToString; + +@ToString +@Setter +public class SceneNPC extends SceneObject{ + public int npc_id; +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGroupSuiteNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGroupSuiteNotify.java new file mode 100644 index 000000000..9abcfc049 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGroupSuiteNotify.java @@ -0,0 +1,25 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.entity.EntityNPC; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GroupSuiteNotifyOuterClass; + +import java.util.List; + +public class PacketGroupSuiteNotify extends BasePacket { + + /** + * control which npc suite is loaded + */ + public PacketGroupSuiteNotify(List list) { + super(PacketOpcodes.GroupSuiteNotify); + + var proto = GroupSuiteNotifyOuterClass.GroupSuiteNotify.newBuilder(); + + list.forEach(item -> proto.putGroupMap(item.getGroupId(), item.getSuiteId())); + + this.setData(proto); + + } +}