Grasscutter/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java

452 lines
14 KiB
Java
Raw Normal View History

package emu.grasscutter.scripts;
import com.github.davidmoten.rtreemulti.RTree;
import com.github.davidmoten.rtreemulti.geometry.Geometry;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.def.MonsterData;
import emu.grasscutter.data.def.WorldLevelData;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.EntityMonster;
2022-05-25 10:44:46 +08:00
import emu.grasscutter.game.entity.EntityNPC;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.VisionTypeOuterClass;
2022-04-29 00:00:23 -07:00
import emu.grasscutter.scripts.constants.EventType;
import emu.grasscutter.scripts.data.*;
import emu.grasscutter.scripts.service.ScriptMonsterSpawnService;
import emu.grasscutter.scripts.service.ScriptMonsterTideService;
2022-05-20 13:46:00 +08:00
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.*;
2022-05-20 13:46:00 +08:00
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
2022-05-23 20:55:22 +08:00
import java.util.stream.Collectors;
public class SceneScriptManager {
private final Scene scene;
2022-04-29 03:06:33 -07:00
private final Map<String, Integer> variables;
private SceneMeta meta;
private boolean isInit;
/**
* current triggers controlled by RefreshGroup
*/
private final Int2ObjectOpenHashMap<Set<SceneTrigger>> currentTriggers;
2022-04-30 01:08:38 -07:00
private final Int2ObjectOpenHashMap<SceneRegion> regions;
private Map<Integer,SceneGroup> sceneGroups;
private ScriptMonsterTideService scriptMonsterTideService;
private ScriptMonsterSpawnService scriptMonsterSpawnService;
/**
* blockid - loaded groupSet
*/
private Int2ObjectMap<Set<SceneGroup>> loadedGroupSetPerBlock;
2022-05-20 13:46:00 +08:00
public static final ExecutorService eventExecutor;
static {
eventExecutor = new ThreadPoolExecutor(4, 4,
2022-05-23 20:55:22 +08:00
60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000),
2022-05-20 13:46:00 +08:00
FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy());
}
public SceneScriptManager(Scene scene) {
this.scene = scene;
this.currentTriggers = new Int2ObjectOpenHashMap<>();
2022-04-30 01:08:38 -07:00
this.regions = new Int2ObjectOpenHashMap<>();
this.variables = new HashMap<>();
this.sceneGroups = new HashMap<>();
this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this);
this.loadedGroupSetPerBlock = new Int2ObjectOpenHashMap<>();
// TEMPORARY
if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) {
return;
}
// Create
this.init();
}
public Scene getScene() {
return scene;
}
public SceneConfig getConfig() {
if(!isInit){
return null;
}
return meta.config;
}
public Map<Integer, SceneBlock> getBlocks() {
return meta.blocks;
}
2022-04-29 03:06:33 -07:00
public Map<String, Integer> getVariables() {
return variables;
}
public Set<SceneTrigger> getTriggersByEvent(int eventId) {
return currentTriggers.computeIfAbsent(eventId, e -> new HashSet<>());
}
2022-05-23 20:55:22 +08:00
public void registerTrigger(List<SceneTrigger> triggers) {
triggers.forEach(this::registerTrigger);
}
public void registerTrigger(SceneTrigger trigger) {
getTriggersByEvent(trigger.event).add(trigger);
}
2022-05-23 20:55:22 +08:00
public void deregisterTrigger(List<SceneTrigger> triggers) {
triggers.forEach(this::deregisterTrigger);
}
public void deregisterTrigger(SceneTrigger trigger) {
getTriggersByEvent(trigger.event).remove(trigger);
}
2022-05-18 15:13:31 +08:00
public void resetTriggers(int eventId) {
currentTriggers.put(eventId, new HashSet<>());
}
public void refreshGroup(SceneGroup group, int suiteIndex){
var suite = group.getSuiteByIndex(suiteIndex);
if(suite == null){
return;
}
2022-05-18 15:13:31 +08:00
if(suite.sceneTriggers.size() > 0){
for(var trigger : suite.sceneTriggers){
resetTriggers(trigger.event);
this.currentTriggers.get(trigger.event).add(trigger);
}
}
spawnMonstersInGroup(group, suite);
spawnGadgetsInGroup(group, suite);
}
2022-04-30 01:08:38 -07:00
public SceneRegion getRegionById(int id) {
return regions.get(id);
}
public void registerRegion(SceneRegion region) {
regions.put(region.config_id, region);
}
public void deregisterRegion(SceneRegion region) {
regions.remove(region.config_id);
}
public Int2ObjectMap<Set<SceneGroup>> getLoadedGroupSetPerBlock() {
return loadedGroupSetPerBlock;
}
// TODO optimize
public SceneGroup getGroupById(int groupId) {
for (SceneBlock block : this.getScene().getLoadedBlocks()) {
2022-05-25 10:44:46 +08:00
var group = block.groups.get(groupId);
if(group == null){
continue;
}
if(!group.isLoaded()){
getScene().onLoadGroup(List.of(group));
}
2022-05-25 10:44:46 +08:00
return group;
}
return null;
}
private void init() {
var meta = ScriptLoader.getSceneMeta(getScene().getId());
if (meta == null){
return;
}
this.meta = meta;
// TEMP
this.isInit = true;
}
public boolean isInit() {
return isInit;
}
public void loadBlockFromScript(SceneBlock block) {
block.load(scene.getId(), meta.context);
}
public void loadGroupFromScript(SceneGroup group) {
group.load(getScene().getId());
if (group.variables != null) {
group.variables.forEach(var -> this.getVariables().put(var.name, var.value));
}
this.sceneGroups.put(group.id, group);
if(group.regions != null){
group.regions.forEach(this::registerRegion);
}
}
2022-04-30 01:08:38 -07:00
public void checkRegions() {
if (this.regions.size() == 0) {
return;
}
for (SceneRegion region : this.regions.values()) {
getScene().getEntities().values()
.stream()
.filter(e -> e.getEntityType() <= 2 && region.contains(e.getPosition()))
.forEach(region::addEntity);
2022-04-30 01:08:38 -07:00
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));
region.resetNewEntities();
}
}
}
2022-05-23 20:55:22 +08:00
public void addGroupSuite(SceneGroup group, SceneSuite suite){
spawnMonstersInGroup(group, suite);
spawnGadgetsInGroup(group, suite);
registerTrigger(suite.sceneTriggers);
}
public void removeGroupSuite(SceneGroup group, SceneSuite suite){
removeMonstersInGroup(group, suite);
removeGadgetsInGroup(group, suite);
deregisterTrigger(suite.sceneTriggers);
}
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) {
2022-05-17 11:17:38 +08:00
var gadgets = group.gadgets.values();
if (suite != null) {
gadgets = suite.sceneGadgets;
}
var toCreate = gadgets.stream()
2022-05-20 13:46:00 +08:00
.map(g -> createGadget(g.group.id, group.block_id, g))
.filter(Objects::nonNull)
.toList();
this.addEntities(toCreate);
2022-04-29 01:03:16 -07:00
}
2022-04-29 03:06:33 -07:00
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;
}
this.addEntities(suite.sceneMonsters.stream()
.map(mob -> createMonster(group.id, group.block_id, mob)).toList());
2022-04-29 03:06:33 -07:00
}
2022-04-29 01:03:16 -07:00
public void spawnMonstersInGroup(SceneGroup group) {
this.addEntities(group.monsters.values().stream()
.map(mob -> createMonster(group.id, group.block_id, mob)).toList());
2022-04-29 03:06:33 -07:00
}
public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
this.scriptMonsterTideService =
new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
}
public void unloadCurrentMonsterTide(){
if(this.getScriptMonsterTideService() == null){
return;
}
this.getScriptMonsterTideService().unload();
}
2022-05-18 15:13:31 +08:00
public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) {
// TODO delay
2022-05-18 15:13:31 +08:00
getScene().addEntity(createMonster(group.id, group.block_id, group.monsters.get(configId)));
2022-04-29 01:03:16 -07:00
}
// Events
2022-05-20 13:46:00 +08:00
public void callEvent(int eventType, ScriptArgs params){
/**
* We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for every groups' trigger in every scene instances.
* But when callEvent is called in a ScriptLib func, it may cause NPE because the inner call cleans the ThreadLocal so that outer call could not get it.
* e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE -> (remove)
* So we use thread pool to clean the stack to avoid this new issue.
*/
eventExecutor.submit(() -> this.realCallEvent(eventType, params));
}
private void realCallEvent(int eventType, ScriptArgs params) {
try{
ScriptLoader.getScriptLib().setSceneScriptManager(this);
for (SceneTrigger trigger : this.getTriggersByEvent(eventType)) {
try{
ScriptLoader.getScriptLib().setCurrentGroup(trigger.currentGroup);
LuaValue ret = callScriptFunc(trigger.condition, trigger.currentGroup, params);
Grasscutter.getLogger().trace("Call Condition Trigger {}", trigger.condition);
if (ret.isboolean() && ret.checkboolean()) {
// the SetGroupVariableValueByGroup in tower need the param to record the first stage time
callScriptFunc(trigger.action, trigger.currentGroup, params);
Grasscutter.getLogger().trace("Call Action Trigger {}", trigger.action);
}
//TODO some ret may not bool
}finally {
ScriptLoader.getScriptLib().removeCurrentGroup();
}
}
}finally {
// make sure it is removed
ScriptLoader.getScriptLib().removeSceneScriptManager();
}
}
2022-05-20 13:46:00 +08:00
private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params){
LuaValue funcLua = null;
if (funcName != null && !funcName.isEmpty()) {
funcLua = (LuaValue) group.getBindings().get(funcName);
}
LuaValue ret = LuaValue.TRUE;
if (funcLua != null) {
LuaValue args = LuaValue.NIL;
if (params != null) {
args = CoerceJavaToLua.coerce(params);
}
ret = safetyCall(funcName, funcLua, args);
}
return ret;
}
public LuaValue safetyCall(String name, LuaValue func, LuaValue args){
try{
return func.call(ScriptLoader.getScriptLibLua(), args);
}catch (LuaError error){
2022-05-23 20:55:22 +08:00
ScriptLib.logger.error("[LUA] call trigger failed {},{}",name,args,error);
return LuaValue.valueOf(-1);
}
}
public ScriptMonsterTideService getScriptMonsterTideService() {
return scriptMonsterTideService;
}
public ScriptMonsterSpawnService getScriptMonsterSpawnService() {
return scriptMonsterSpawnService;
}
2022-05-18 15:13:31 +08:00
public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) {
EntityGadget entity = new EntityGadget(getScene(), g.gadget_id, g.pos);
if (entity.getGadgetData() == null){
return null;
}
entity.setBlockId(blockId);
entity.setConfigId(g.config_id);
entity.setGroupId(groupId);
entity.getRotation().set(g.rot);
entity.setState(g.state);
2022-05-18 02:21:34 -07:00
entity.setPointType(g.point_type);
2022-05-20 13:46:00 +08:00
entity.setMetaGadget(g);
2022-05-18 02:21:34 -07:00
entity.buildContent();
return entity;
}
2022-05-25 10:44:46 +08:00
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;
}
MonsterData data = GameData.getMonsterDataMap().get(monster.monster_id);
if (data == null) {
return null;
}
// Calculate level
int level = monster.level;
if (getScene().getDungeonData() != null) {
level = getScene().getDungeonData().getShowLevel();
} else if (getScene().getWorld().getWorldLevel() > 0) {
WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getScene().getWorld().getWorldLevel());
if (worldLevelData != null) {
level = worldLevelData.getMonsterLevel();
}
}
// Spawn mob
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level);
entity.getRotation().set(monster.rot);
entity.setGroupId(groupId);
entity.setBlockId(blockId);
entity.setConfigId(monster.config_id);
2022-05-23 20:55:22 +08:00
entity.setPoseId(monster.pose_id);
this.getScriptMonsterSpawnService()
.onMonsterCreatedListener.forEach(action -> action.onNotify(entity));
return entity;
}
public void addEntity(GameEntity gameEntity){
getScene().addEntity(gameEntity);
}
public void meetEntities(List<? extends GameEntity> gameEntity){
getScene().addEntities(gameEntity, VisionTypeOuterClass.VisionType.VISION_MEET);
}
public void addEntities(List<? extends GameEntity> gameEntity){
getScene().addEntities(gameEntity);
}
public RTree<SceneBlock, Geometry> getBlocksIndex() {
return meta.sceneBlockIndex;
}
2022-05-23 20:55:22 +08:00
public void removeMonstersInGroup(SceneGroup group, SceneSuite suite) {
var configSet = suite.sceneMonsters.stream()
.map(m -> m.config_id)
.collect(Collectors.toSet());
var toRemove = getScene().getEntities().values().stream()
.filter(e -> e instanceof EntityMonster)
.filter(e -> e.getGroupId() == group.id)
.filter(e -> configSet.contains(e.getConfigId()))
.toList();
getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_MISS);
}
public void removeGadgetsInGroup(SceneGroup group, SceneSuite suite) {
var configSet = suite.sceneGadgets.stream()
.map(m -> m.config_id)
.collect(Collectors.toSet());
var toRemove = getScene().getEntities().values().stream()
.filter(e -> e instanceof EntityGadget)
.filter(e -> e.getGroupId() == group.id)
.filter(e -> configSet.contains(e.getConfigId()))
.toList();
getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_MISS);
}
}