Skip to content
Snippets Groups Projects
Commit d95708ec authored by Akka's avatar Akka Committed by Melledy
Browse files

Support spawn NPC

parent 5a4a7089
No related merge requests found
Showing
with 288 additions and 32 deletions
......@@ -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<String, OpenConfigEntry> openConfigEntries = new HashMap<>();
private static final Map<String, ScenePointEntry> scenePointEntries = new HashMap<>();
private static final Int2ObjectMap<MainQuestData> mainQuestData = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<SceneNpcBornData> npcBornData = new Int2ObjectOpenHashMap<>();
// ExcelConfigs
private static final Int2ObjectMap<PlayerLevelData> playerLevelDataMap = new Int2ObjectOpenHashMap<>();
......@@ -131,7 +128,9 @@ public class GameData {
public static Int2ObjectMap<MainQuestData> getMainQuestDataMap() {
return mainQuestData;
}
public static Int2ObjectMap<SceneNpcBornData> getSceneNpcBornData() {
return npcBornData;
}
public static Int2ObjectMap<AvatarData> getAvatarDataMap() {
return avatarDataMap;
}
......
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<SceneNpcBornEntry> 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 {
......
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<SceneNpcBornEntry> bornPosList;
/**
* Spatial Index For NPC
*/
transient PhTree<SceneNpcBornEntry> index;
/**
* npc groups
*/
transient Map<Integer, SceneGroup> groups = new ConcurrentHashMap<>();
}
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<Integer> suiteIdList;
}
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();
}
}
......@@ -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<SceneGadget> 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<EntityNPC> 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;
}
}
......@@ -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 <T> void buildIndex(PhTree<T> tree, List<T> elements, Function<T, long[]> extractor){
public static <T> void buildIndex(PhTree<T> tree, Collection<T> elements, Function<T, long[]> extractor){
elements.forEach(e -> tree.put(extractor.apply(e), e));
}
public static <T> List<T> queryNeighbors(PhTree<T> tree, Position position, int range){
......
......@@ -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;
......
......@@ -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<SceneGroup> groups;
public Map<Integer,SceneGroup> groups;
public PhTree<SceneGroup> 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);
}
......
......@@ -32,7 +32,7 @@ public class SceneGroup {
public Map<Integer,SceneMonster> monsters; // <ConfigId, Monster>
public Map<Integer, SceneGadget> gadgets; // <ConfigId, Gadgets>
public Map<String, SceneTrigger> triggers;
public Map<Integer, SceneNPC> npc; // <NpcId, NPC>
public List<SceneRegion> regions;
public List<SceneSuite> suites;
public List<SceneVar> 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) {
......
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
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<EntityNPC> list) {
super(PacketOpcodes.GroupSuiteNotify);
var proto = GroupSuiteNotifyOuterClass.GroupSuiteNotify.newBuilder();
list.forEach(item -> proto.putGroupMap(item.getGroupId(), item.getSuiteId()));
this.setData(proto);
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment