SceneScriptManager.java 17.1 KB
Newer Older
1
2
package emu.grasscutter.scripts;

3
4
import com.github.davidmoten.rtreemulti.RTree;
import com.github.davidmoten.rtreemulti.geometry.Geometry;
5
import emu.grasscutter.Grasscutter;
Akka's avatar
Akka committed
6
import emu.grasscutter.data.GameData;
Melledy's avatar
Melledy committed
7
8
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.WorldLevelData;
Akka's avatar
Akka committed
9
import emu.grasscutter.game.entity.*;
Akka's avatar
Akka committed
10
import emu.grasscutter.game.props.EntityType;
11
import emu.grasscutter.game.world.Scene;
Akka's avatar
Akka committed
12
import emu.grasscutter.net.proto.VisionTypeOuterClass;
Melledy's avatar
Melledy committed
13
import emu.grasscutter.scripts.constants.EventType;
Akka's avatar
Akka committed
14
15
16
import emu.grasscutter.scripts.data.*;
import emu.grasscutter.scripts.service.ScriptMonsterSpawnService;
import emu.grasscutter.scripts.service.ScriptMonsterTideService;
Akka's avatar
Akka committed
17
import io.netty.util.concurrent.FastThreadLocalThread;
Akka's avatar
Akka committed
18
19
20
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
21

Akka's avatar
Akka committed
22
import java.util.*;
Akka's avatar
Akka committed
23
import java.util.concurrent.*;
Akka's avatar
Akka committed
24
import java.util.stream.Collectors;
25

26
27
public class SceneScriptManager {
	private final Scene scene;
Melledy's avatar
Melledy committed
28
	private final Map<String, Integer> variables;
Akka's avatar
Akka committed
29
	private SceneMeta meta;
30
	private boolean isInit;
31
32
33
	/**
	 * current triggers controlled by RefreshGroup
	 */
Akka's avatar
Akka committed
34
35
36
	private final Map<Integer, Set<SceneTrigger>> currentTriggers;
    private final Map<Integer, EntityRegion> regions; // <EntityId-Region>
	private final Map<Integer,SceneGroup> sceneGroups;
37
38
	private ScriptMonsterTideService scriptMonsterTideService;
	private ScriptMonsterSpawnService scriptMonsterSpawnService;
Akka's avatar
Akka committed
39
40
41
	/**
	 * blockid - loaded groupSet
	 */
Akka's avatar
Akka committed
42
	private final Map<Integer, Set<SceneGroup>> loadedGroupSetPerBlock;
Akka's avatar
Akka committed
43
44
45
	public static final ExecutorService eventExecutor;
	static {
		eventExecutor = new ThreadPoolExecutor(4, 4,
Akka's avatar
Akka committed
46
				60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000),
Akka's avatar
Akka committed
47
48
				FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy());
	}
49
50
	public SceneScriptManager(Scene scene) {
		this.scene = scene;
Akka's avatar
Akka committed
51
		this.currentTriggers = new ConcurrentHashMap<>();
52

Akka's avatar
Akka committed
53
54
55
		this.regions = new ConcurrentHashMap<>();
		this.variables = new ConcurrentHashMap<>();
		this.sceneGroups = new ConcurrentHashMap<>();
56
		this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this);
Akka's avatar
Akka committed
57
		this.loadedGroupSetPerBlock = new ConcurrentHashMap<>();
58

59
		// TEMPORARY
Akka's avatar
Akka committed
60
		if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) {
61
62
			return;
		}
Akka's avatar
Akka committed
63

64
65
66
		// Create
		this.init();
	}
Akka's avatar
Akka committed
67

68
69
70
71
72
	public Scene getScene() {
		return scene;
	}

	public SceneConfig getConfig() {
Akka's avatar
Akka committed
73
74
75
76
		if(!isInit){
			return null;
		}
		return meta.config;
77
78
	}

Akka's avatar
Akka committed
79
80
	public Map<Integer, SceneBlock> getBlocks() {
		return meta.blocks;
81
82
	}

Melledy's avatar
Melledy committed
83
	public Map<String, Integer> getVariables() {
84
85
86
		return variables;
	}

87
	public Set<SceneTrigger> getTriggersByEvent(int eventId) {
88
		return currentTriggers.computeIfAbsent(eventId, e -> new HashSet<>());
89
	}
Akka's avatar
Akka committed
90
91
92
	public void registerTrigger(List<SceneTrigger> triggers) {
		triggers.forEach(this::registerTrigger);
	}
93
94
	public void registerTrigger(SceneTrigger trigger) {
		getTriggersByEvent(trigger.event).add(trigger);
akatatsu27's avatar
akatatsu27 committed
95
        Grasscutter.getLogger().debug("Registered trigger {}", trigger.name);
96
	}
Akka's avatar
Akka committed
97
98
99
	public void deregisterTrigger(List<SceneTrigger> triggers) {
		triggers.forEach(this::deregisterTrigger);
	}
100
101
102
	public void deregisterTrigger(SceneTrigger trigger) {
		getTriggersByEvent(trigger.event).remove(trigger);
	}
Akka's avatar
Akka committed
103
104
	public void resetTriggers(int eventId) {
		currentTriggers.put(eventId, new HashSet<>());
105
106
107
108
109
110
	}
	public void refreshGroup(SceneGroup group, int suiteIndex){
		var suite = group.getSuiteByIndex(suiteIndex);
		if(suite == null){
			return;
		}
Akka's avatar
Akka committed
111
112
113
114
115
		if(suite.sceneTriggers.size() > 0){
			for(var trigger : suite.sceneTriggers){
				resetTriggers(trigger.event);
				this.currentTriggers.get(trigger.event).add(trigger);
			}
116
117
118
119
		}
		spawnMonstersInGroup(group, suite);
		spawnGadgetsInGroup(group, suite);
	}
Akka's avatar
Akka committed
120
	public EntityRegion getRegionById(int id) {
Melledy's avatar
Melledy committed
121
122
		return regions.get(id);
	}
Akka's avatar
Akka committed
123
124
125

	public void registerRegion(EntityRegion region) {
		regions.put(region.getId(), region);
akatatsu27's avatar
akatatsu27 committed
126
        Grasscutter.getLogger().debug("Registered region {} from group {}", region.getMetaRegion().config_id, region.getGroupId());
Akka's avatar
Akka committed
127
128
129
130
131
132
133
134
135
136
	}
    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()));
Melledy's avatar
Melledy committed
137
	}
Akka's avatar
Akka committed
138

Akka's avatar
Akka committed
139
	public Map<Integer, Set<SceneGroup>> getLoadedGroupSetPerBlock() {
Akka's avatar
Akka committed
140
141
142
		return loadedGroupSetPerBlock;
	}

143
144
145
	// TODO optimize
	public SceneGroup getGroupById(int groupId) {
		for (SceneBlock block : this.getScene().getLoadedBlocks()) {
Akka's avatar
Akka committed
146
147
148
149
150
151
152
			var group = block.groups.get(groupId);
			if(group == null){
				continue;
			}

			if(!group.isLoaded()){
				getScene().onLoadGroup(List.of(group));
153
			}
Akka's avatar
Akka committed
154
			return group;
155
156
157
158
159
		}
		return null;
	}

	private void init() {
Akka's avatar
Akka committed
160
161
		var meta = ScriptLoader.getSceneMeta(getScene().getId());
		if (meta == null){
162
			return;
163
		}
Akka's avatar
Akka committed
164
165
		this.meta = meta;

166
167
168
169
170
171
172
		// TEMP
		this.isInit = true;
	}

	public boolean isInit() {
		return isInit;
	}
Akka's avatar
Akka committed
173

Akka's avatar
Akka committed
174
175
	public void loadBlockFromScript(SceneBlock block) {
		block.load(scene.getId(), meta.context);
176
	}
Akka's avatar
Akka committed
177

178
	public void loadGroupFromScript(SceneGroup group) {
179
		group.load(getScene().getId());
180

181
182
183
184
		if (group.variables != null) {
			group.variables.forEach(var -> this.getVariables().put(var.name, var.value));
		}

Akka's avatar
Akka committed
185
		this.sceneGroups.put(group.id, group);
186
	}
Akka's avatar
Akka committed
187

Melledy's avatar
Melledy committed
188
189
190
191
	public void checkRegions() {
		if (this.regions.size() == 0) {
			return;
		}
Akka's avatar
Akka committed
192
193

		for (var region : this.regions.values()) {
Akka's avatar
Akka committed
194
            // currently all condition_ENTER_REGION Events check for avatar, so we have no necessary to add other types of entity
Melledy's avatar
Melledy committed
195
196
			getScene().getEntities().values()
				.stream()
Akka's avatar
Akka committed
197
				.filter(e -> e.getEntityType() == EntityType.Avatar.getValue() && region.getMetaRegion().contains(e.getPosition()))
Melledy's avatar
Melledy committed
198
				.forEach(region::addEntity);
199

akatatsu27's avatar
akatatsu27 committed
200
201
202
203
204
            var players = region.getScene().getPlayers();
            int targetID = 0;
            if(players.size() > 0)
                targetID = players.get(0).getUid();

Melledy's avatar
Melledy committed
205
			if (region.hasNewEntities()) {
akatatsu27's avatar
akatatsu27 committed
206
                Grasscutter.getLogger().trace("Call EVENT_ENTER_REGION_{}",region.getMetaRegion().config_id);
Akka's avatar
Akka committed
207
208
				callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.getConfigId())
                    .setSourceEntityId(region.getId())
akatatsu27's avatar
akatatsu27 committed
209
210
211
212
213
214
215
                    .setTargetEntityId(targetID)
                );

				region.resetNewEntities();
			}

            for(int entityId : region.getEntities()) {
akatatsu27's avatar
akatatsu27 committed
216
                if(getScene().getEntityById(entityId) == null || !region.getMetaRegion().contains(getScene().getEntityById(entityId).getPosition())) {
akatatsu27's avatar
akatatsu27 committed
217
218
219
220
221
222
223
                    region.removeEntity(entityId);

                }
            }
            if (region.entityLeave()) {
                callEvent(EventType.EVENT_LEAVE_REGION, new ScriptArgs(region.getConfigId())
                    .setSourceEntityId(region.getId())
Akka's avatar
Akka committed
224
225
                    .setTargetEntityId(region.getFirstEntityId())
                );
Akka's avatar
Akka committed
226

Melledy's avatar
Melledy committed
227
228
229
				region.resetNewEntities();
			}
		}
230
	}
Akka's avatar
Akka committed
231

Akka's avatar
Akka committed
232
233
234
235
236
237
238
239
240
241
242
243
    public List<EntityGadget> getGadgetsInGroupSuite(SceneGroup group, SceneSuite suite){
        return suite.sceneGadgets.stream()
            .map(g -> createGadget(group.id, group.block_id, g))
            .filter(Objects::nonNull)
            .toList();
    }
    public List<EntityMonster> getMonstersInGroupSuite(SceneGroup group, SceneSuite suite){
        return suite.sceneMonsters.stream()
            .map(mob -> createMonster(group.id, group.block_id, mob))
            .filter(Objects::nonNull)
            .toList();
    }
Akka's avatar
Akka committed
244
	public void addGroupSuite(SceneGroup group, SceneSuite suite){
Akka's avatar
Akka committed
245
246
247
248
249
250
251
252
253
        // we added trigger first
        registerTrigger(suite.sceneTriggers);

        var toCreate = new ArrayList<GameEntity>();
        toCreate.addAll(getGadgetsInGroupSuite(group, suite));
        toCreate.addAll(getMonstersInGroupSuite(group, suite));
        addEntities(toCreate);

        registerRegionInGroupSuite(group, suite);
Akka's avatar
Akka committed
254
255
	}
	public void removeGroupSuite(SceneGroup group, SceneSuite suite){
Akka's avatar
Akka committed
256
        deregisterTrigger(suite.sceneTriggers);
Akka's avatar
Akka committed
257
258
		removeMonstersInGroup(group, suite);
		removeGadgetsInGroup(group, suite);
Akka's avatar
Akka committed
259
260

        suite.sceneRegions.forEach(this::deregisterRegion);
261
	}
Akka's avatar
Akka committed
262

263
	public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) {
Akka's avatar
Akka committed
264
		var gadgets = group.gadgets.values();
Akka's avatar
Akka committed
265

266
267
268
		if (suite != null) {
			gadgets = suite.sceneGadgets;
		}
Akka's avatar
Akka committed
269
270

		var toCreate = gadgets.stream()
Akka's avatar
Akka committed
271
				.map(g -> createGadget(g.group.id, group.block_id, g))
Akka's avatar
Akka committed
272
273
274
				.filter(Objects::nonNull)
				.toList();
		this.addEntities(toCreate);
Melledy's avatar
Melledy committed
275
	}
276
277
278
279

	public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) {
		if(suite == null || suite.sceneMonsters.size() <= 0){
			return;
280
		}
Akka's avatar
Akka committed
281
282
		this.addEntities(suite.sceneMonsters.stream()
				.map(mob -> createMonster(group.id, group.block_id, mob)).toList());
Melledy's avatar
Melledy committed
283
	}
284

285
286
287
	public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
		this.scriptMonsterTideService =
				new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
288
289

	}
290
291
292
293
294
295
	public void unloadCurrentMonsterTide(){
		if(this.getScriptMonsterTideService() == null){
			return;
		}
		this.getScriptMonsterTideService().unload();
	}
Akka's avatar
Akka committed
296
	public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) {
297
		// TODO delay
Akka's avatar
Akka committed
298
		getScene().addEntity(createMonster(group.id, group.block_id, group.monsters.get(configId)));
Melledy's avatar
Melledy committed
299
	}
300
	// Events
Akka's avatar
Akka committed
301
302
303
304
305
306
307
308
309
310
311
	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) {
akatatsu27's avatar
akatatsu27 committed
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
        try {
            Set<SceneTrigger> relevantTriggers = new HashSet<>();
            if(eventType == EventType.EVENT_ENTER_REGION || eventType == EventType.EVENT_LEAVE_REGION) {
                List<SceneTrigger> relevantTriggersList = this.getTriggersByEvent(eventType).stream()
                    .filter(p -> p.condition.contains(String.valueOf(params.param1))).toList();
                relevantTriggers = new HashSet<>(relevantTriggersList);
            } else {relevantTriggers = this.getTriggersByEvent(eventType);}
            for (SceneTrigger trigger : relevantTriggers) {
                try {
                    ScriptLoader.getScriptLib().setCurrentGroup(trigger.currentGroup);
                    LuaValue ret = this.callScriptFunc(trigger.condition, trigger.currentGroup, params);
                    Grasscutter.getLogger().trace("Call Condition Trigger {}, [{},{},{}]", trigger.condition, params.param1, params.source_eid, params.target_eid);
                    if (ret.isboolean() && ret.checkboolean()) {
                        // the SetGroupVariableValueByGroup in tower need the param to record the first stage time
                        this.callScriptFunc(trigger.action, trigger.currentGroup, params);
                        Grasscutter.getLogger().trace("Call Action Trigger {}", trigger.action);
                        if (trigger.event == EventType.EVENT_ENTER_REGION) {
                            EntityRegion region = this.regions.values().stream().filter(p -> p.getConfigId() == params.param1).toList().get(0);
                            getScene().getPlayers().forEach(p -> p.onEnterRegion(region.getMetaRegion()));
331
                            deregisterRegion(region.getMetaRegion());
akatatsu27's avatar
akatatsu27 committed
332
333
334
                        } else if (trigger.event == EventType.EVENT_LEAVE_REGION) {
                            EntityRegion region = this.regions.values().stream().filter(p -> p.getConfigId() == params.param1).toList().get(0);
                            getScene().getPlayers().forEach(p -> p.onLeaveRegion(region.getMetaRegion()));
335
                            deregisterRegion(region.getMetaRegion());
akatatsu27's avatar
akatatsu27 committed
336
337
338
339
340
341
342
343
344
345
346
                        }
                        deregisterTrigger(trigger);
                    } else {
                        Grasscutter.getLogger().debug("Condition Trigger {} returned {}", trigger.condition, ret);
                    }
                    //TODO some ret do not bool
                }finally {
                    ScriptLoader.getScriptLib().removeCurrentGroup();
                }
            }
        }finally {
347
348
349
350
351
			// make sure it is removed
			ScriptLoader.getScriptLib().removeSceneScriptManager();
		}
	}

Akka's avatar
Akka committed
352
	private LuaValue callScriptFunc(String funcName, SceneGroup group, ScriptArgs params){
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
		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);
368
		}
369
		return ret;
370
	}
371

372
373
	public LuaValue safetyCall(String name, LuaValue func, LuaValue args){
		try{
374
			return func.call(ScriptLoader.getScriptLibLua(), args);
375
		}catch (LuaError error){
Akka's avatar
Akka committed
376
			ScriptLib.logger.error("[LUA] call trigger failed {},{}",name,args,error);
377
378
379
380
381
382
383
384
385
386
387
388
			return LuaValue.valueOf(-1);
		}
	}

	public ScriptMonsterTideService getScriptMonsterTideService() {
		return scriptMonsterTideService;
	}

	public ScriptMonsterSpawnService getScriptMonsterSpawnService() {
		return scriptMonsterSpawnService;
	}

Akka's avatar
Akka committed
389
	public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) {
Akka's avatar
Akka committed
390
391
392
393
394
395
396
397
398
399
        if(g.isOneoff){
            var hasEntity = getScene().getEntities().values().stream()
                .filter(e -> e instanceof EntityGadget)
                .filter(e -> e.getGroupId() == g.group.id)
                .filter(e -> e.getConfigId() == g.config_id)
                .findFirst();
            if(hasEntity.isPresent()){
                return null;
            }
        }
Akka's avatar
Akka committed
400
401
402
403
404
405
406
407
408
409
410
		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);
Melledy's avatar
Melledy committed
411
		entity.setPointType(g.point_type);
Akka's avatar
Akka committed
412
		entity.setMetaGadget(g);
Melledy's avatar
Melledy committed
413
		entity.buildContent();
Akka's avatar
Akka committed
414
415
416

		return entity;
	}
Akka's avatar
Akka committed
417
418
419
	public EntityNPC createNPC(SceneNPC npc, int blockId, int suiteId) {
		return new EntityNPC(getScene(), npc, blockId, suiteId);
	}
Akka's avatar
Akka committed
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
	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);
Akka's avatar
Akka committed
450
		entity.setPoseId(monster.pose_id);
Akka's avatar
Akka committed
451
452
453

		this.getScriptMonsterSpawnService()
				.onMonsterCreatedListener.forEach(action -> action.onNotify(entity));
Akka's avatar
Akka committed
454

Akka's avatar
Akka committed
455
456
457
458
459
460
		return entity;
	}

	public void addEntity(GameEntity gameEntity){
		getScene().addEntity(gameEntity);
	}
Akka's avatar
Akka committed
461

Akka's avatar
Akka committed
462
	public void meetEntities(List<? extends GameEntity> gameEntity){
Melledy's avatar
Melledy committed
463
		getScene().addEntities(gameEntity, VisionTypeOuterClass.VisionType.VISION_TYPE_MEET);
Akka's avatar
Akka committed
464
	}
Akka's avatar
Akka committed
465

Akka's avatar
Akka committed
466
467
468
469
	public void addEntities(List<? extends GameEntity> gameEntity){
		getScene().addEntities(gameEntity);
	}

470
	public RTree<SceneBlock, Geometry> getBlocksIndex() {
Akka's avatar
Akka committed
471
472
		return meta.sceneBlockIndex;
	}
Akka's avatar
Akka committed
473
474
475
476
477
478
479
480
481
482
	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();

Melledy's avatar
Melledy committed
483
		getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
Akka's avatar
Akka committed
484
485
486
487
488
489
490
491
492
493
494
	}
	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();

Melledy's avatar
Melledy committed
495
		getScene().removeEntities(toRemove, VisionTypeOuterClass.VisionType.VISION_TYPE_MISS);
Akka's avatar
Akka committed
496
	}
497
}