mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-23 13:37:42 +00:00
add region entity
This commit is contained in:
parent
e499601283
commit
7e7f1e11e0
74
src/main/java/emu/grasscutter/game/entity/EntityRegion.java
Normal file
74
src/main/java/emu/grasscutter/game/entity/EntityRegion.java
Normal file
@ -0,0 +1,74 @@
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import emu.grasscutter.game.props.EntityIdType;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass;
|
||||
import emu.grasscutter.scripts.data.SceneRegion;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Getter
|
||||
public class EntityRegion extends GameEntity{
|
||||
private final Position position;
|
||||
private boolean hasNewEntities;
|
||||
private final Set<Integer> entities; // Ids of entities inside this region
|
||||
private final SceneRegion metaRegion;
|
||||
|
||||
public EntityRegion(Scene scene, SceneRegion region) {
|
||||
super(scene);
|
||||
this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION);
|
||||
setGroupId(region.group.id);
|
||||
setBlockId(region.group.block_id);
|
||||
setConfigId(region.config_id);
|
||||
this.position = region.pos.clone();
|
||||
this.entities = ConcurrentHashMap.newKeySet();
|
||||
this.metaRegion = region;
|
||||
}
|
||||
|
||||
public void addEntity(GameEntity entity) {
|
||||
if (this.getEntities().contains(entity.getId())) {
|
||||
return;
|
||||
}
|
||||
this.getEntities().add(entity.getId());
|
||||
this.hasNewEntities = true;
|
||||
}
|
||||
|
||||
public boolean hasNewEntities() {
|
||||
return hasNewEntities;
|
||||
}
|
||||
|
||||
public void resetNewEntities() {
|
||||
hasNewEntities = false;
|
||||
}
|
||||
|
||||
public void removeEntity(GameEntity entity) {
|
||||
this.getEntities().remove(entity.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Int2FloatOpenHashMap getFightProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getRotation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SceneEntityInfoOuterClass.SceneEntityInfo toProto() {
|
||||
/**
|
||||
* The Region Entity would not be sent to client.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ public enum EntityIdType {
|
||||
MONSTER (0x02),
|
||||
NPC (0x03),
|
||||
GADGET (0x04),
|
||||
WEAPON (0x06),
|
||||
REGION (0x05),
|
||||
WEAPON (0x06),
|
||||
TEAM (0x09),
|
||||
MPLEVEL (0x0b);
|
||||
|
||||
|
@ -30,14 +30,15 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import org.danilopianini.util.SpatialIndex;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Scene {
|
||||
private final World world;
|
||||
private final SceneData sceneData;
|
||||
private final List<Player> players;
|
||||
private final Int2ObjectMap<GameEntity> entities;
|
||||
|
||||
private final Map<Integer, GameEntity> entities;
|
||||
private final Set<SpawnDataEntry> spawnedEntities;
|
||||
private final Set<SpawnDataEntry> deadSpawnedEntities;
|
||||
private final Set<SceneBlock> loadedBlocks;
|
||||
@ -57,16 +58,16 @@ public class Scene {
|
||||
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.players = new CopyOnWriteArrayList<>();
|
||||
this.entities = new ConcurrentHashMap<>();
|
||||
|
||||
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.spawnedEntities = ConcurrentHashMap.newKeySet();
|
||||
this.deadSpawnedEntities = ConcurrentHashMap.newKeySet();
|
||||
this.loadedBlocks = ConcurrentHashMap.newKeySet();
|
||||
this.scriptManager = new SceneScriptManager(this);
|
||||
}
|
||||
|
||||
@ -94,7 +95,7 @@ public class Scene {
|
||||
return this.getPlayers().size();
|
||||
}
|
||||
|
||||
public Int2ObjectMap<GameEntity> getEntities() {
|
||||
public Map<Integer, GameEntity> getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
@ -629,15 +630,10 @@ public class Scene {
|
||||
var suiteData = group.getSuiteByIndex(suite);
|
||||
suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger);
|
||||
|
||||
entities.addAll(suiteData.sceneGadgets.stream()
|
||||
.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))
|
||||
.filter(Objects::nonNull)
|
||||
.toList());
|
||||
entities.addAll(scriptManager.getGadgetsInGroupSuite(group, suiteData));
|
||||
entities.addAll(scriptManager.getMonstersInGroupSuite(group, suiteData));
|
||||
|
||||
scriptManager.registerRegionInGroupSuite(group, suiteData);
|
||||
}
|
||||
|
||||
scriptManager.meetEntities(entities);
|
||||
@ -660,13 +656,12 @@ public class Scene {
|
||||
group.triggers.values().forEach(getScriptManager()::deregisterTrigger);
|
||||
}
|
||||
if(group.regions != null){
|
||||
group.regions.forEach(getScriptManager()::deregisterRegion);
|
||||
group.regions.values().forEach(getScriptManager()::deregisterRegion);
|
||||
}
|
||||
}
|
||||
scriptManager.getLoadedGroupSetPerBlock().remove(block.id);
|
||||
Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id);
|
||||
}
|
||||
|
||||
// Gadgets
|
||||
|
||||
public void onPlayerCreateGadget(EntityClientGadget gadget) {
|
||||
|
@ -6,10 +6,7 @@ import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.MonsterData;
|
||||
import emu.grasscutter.data.excels.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.entity.*;
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.net.proto.VisionTypeOuterClass;
|
||||
import emu.grasscutter.scripts.constants.EventType;
|
||||
@ -17,17 +14,12 @@ import emu.grasscutter.scripts.data.*;
|
||||
import emu.grasscutter.scripts.service.ScriptMonsterSpawnService;
|
||||
import emu.grasscutter.scripts.service.ScriptMonsterTideService;
|
||||
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.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SceneScriptManager {
|
||||
@ -38,15 +30,15 @@ public class SceneScriptManager {
|
||||
/**
|
||||
* current triggers controlled by RefreshGroup
|
||||
*/
|
||||
private final Int2ObjectOpenHashMap<Set<SceneTrigger>> currentTriggers;
|
||||
private final Int2ObjectOpenHashMap<SceneRegion> regions;
|
||||
private Map<Integer,SceneGroup> sceneGroups;
|
||||
private final Map<Integer, Set<SceneTrigger>> currentTriggers;
|
||||
private final Map<Integer, EntityRegion> regions; // <EntityId-Region>
|
||||
private final Map<Integer,SceneGroup> sceneGroups;
|
||||
private ScriptMonsterTideService scriptMonsterTideService;
|
||||
private ScriptMonsterSpawnService scriptMonsterSpawnService;
|
||||
/**
|
||||
* blockid - loaded groupSet
|
||||
*/
|
||||
private Int2ObjectMap<Set<SceneGroup>> loadedGroupSetPerBlock;
|
||||
private final Map<Integer, Set<SceneGroup>> loadedGroupSetPerBlock;
|
||||
public static final ExecutorService eventExecutor;
|
||||
static {
|
||||
eventExecutor = new ThreadPoolExecutor(4, 4,
|
||||
@ -55,13 +47,13 @@ public class SceneScriptManager {
|
||||
}
|
||||
public SceneScriptManager(Scene scene) {
|
||||
this.scene = scene;
|
||||
this.currentTriggers = new Int2ObjectOpenHashMap<>();
|
||||
this.currentTriggers = new ConcurrentHashMap<>();
|
||||
|
||||
this.regions = new Int2ObjectOpenHashMap<>();
|
||||
this.variables = new HashMap<>();
|
||||
this.sceneGroups = new HashMap<>();
|
||||
this.regions = new ConcurrentHashMap<>();
|
||||
this.variables = new ConcurrentHashMap<>();
|
||||
this.sceneGroups = new ConcurrentHashMap<>();
|
||||
this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this);
|
||||
this.loadedGroupSetPerBlock = new Int2ObjectOpenHashMap<>();
|
||||
this.loadedGroupSetPerBlock = new ConcurrentHashMap<>();
|
||||
|
||||
// TEMPORARY
|
||||
if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) {
|
||||
@ -123,19 +115,25 @@ public class SceneScriptManager {
|
||||
spawnMonstersInGroup(group, suite);
|
||||
spawnGadgetsInGroup(group, suite);
|
||||
}
|
||||
public SceneRegion getRegionById(int id) {
|
||||
public EntityRegion getRegionById(int id) {
|
||||
return regions.get(id);
|
||||
}
|
||||
|
||||
public void registerRegion(SceneRegion region) {
|
||||
regions.put(region.config_id, region);
|
||||
public void registerRegion(EntityRegion region) {
|
||||
regions.put(region.getId(), region);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
|
||||
public void deregisterRegion(SceneRegion region) {
|
||||
regions.remove(region.config_id);
|
||||
}
|
||||
|
||||
public Int2ObjectMap<Set<SceneGroup>> getLoadedGroupSetPerBlock() {
|
||||
public Map<Integer, Set<SceneGroup>> getLoadedGroupSetPerBlock() {
|
||||
return loadedGroupSetPerBlock;
|
||||
}
|
||||
|
||||
@ -182,10 +180,6 @@ public class SceneScriptManager {
|
||||
}
|
||||
|
||||
this.sceneGroups.put(group.id, group);
|
||||
|
||||
if(group.regions != null){
|
||||
group.regions.forEach(this::registerRegion);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkRegions() {
|
||||
@ -193,37 +187,49 @@ public class SceneScriptManager {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SceneRegion region : this.regions.values()) {
|
||||
for (var region : this.regions.values()) {
|
||||
getScene().getEntities().values()
|
||||
.stream()
|
||||
.filter(e -> e.getEntityType() <= 2 && region.contains(e.getPosition()))
|
||||
.filter(e -> e.getEntityType() <= 2 && region.getMetaRegion().contains(e.getPosition()))
|
||||
.forEach(region::addEntity);
|
||||
|
||||
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));
|
||||
callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.getConfigId()).setSourceEntityId(region.getId()));
|
||||
|
||||
region.resetNewEntities();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
public void addGroupSuite(SceneGroup group, SceneSuite suite){
|
||||
spawnMonstersInGroup(group, suite);
|
||||
spawnGadgetsInGroup(group, suite);
|
||||
registerTrigger(suite.sceneTriggers);
|
||||
// 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);
|
||||
}
|
||||
public void removeGroupSuite(SceneGroup group, SceneSuite suite){
|
||||
deregisterTrigger(suite.sceneTriggers);
|
||||
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);
|
||||
suite.sceneRegions.forEach(this::deregisterRegion);
|
||||
}
|
||||
|
||||
public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) {
|
||||
@ -240,13 +246,6 @@ public class SceneScriptManager {
|
||||
this.addEntities(toCreate);
|
||||
}
|
||||
|
||||
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;
|
||||
@ -255,11 +254,6 @@ public class SceneScriptManager {
|
||||
.map(mob -> createMonster(group.id, group.block_id, mob)).toList());
|
||||
}
|
||||
|
||||
public void spawnMonstersInGroup(SceneGroup group) {
|
||||
this.addEntities(group.monsters.values().stream()
|
||||
.map(mob -> createMonster(group.id, group.block_id, mob)).toList());
|
||||
}
|
||||
|
||||
public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
|
||||
this.scriptMonsterTideService =
|
||||
new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
|
||||
|
@ -316,13 +316,13 @@ public class ScriptLib {
|
||||
int regionId = table.get("region_eid").toint();
|
||||
int entityType = table.get("entity_type").toint();
|
||||
|
||||
SceneRegion region = this.getSceneScriptManager().getRegionById(regionId);
|
||||
var region = this.getSceneScriptManager().getRegionById(regionId);
|
||||
|
||||
if (region == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) region.getEntities().intStream().filter(e -> e >> 24 == entityType).count();
|
||||
return (int) region.getEntities().stream().filter(e -> e >> 24 == entityType).count();
|
||||
}
|
||||
|
||||
public void PrintContextLog(String msg) {
|
||||
|
@ -34,7 +34,7 @@ public class SceneGroup {
|
||||
public Map<Integer, SceneGadget> gadgets; // <ConfigId, Gadgets>
|
||||
public Map<String, SceneTrigger> triggers;
|
||||
public Map<Integer, SceneNPC> npc; // <NpcId, NPC>
|
||||
public List<SceneRegion> regions;
|
||||
public Map<Integer, SceneRegion> regions;
|
||||
public List<SceneSuite> suites;
|
||||
public List<SceneVar> variables;
|
||||
|
||||
@ -115,7 +115,10 @@ public class SceneGroup {
|
||||
triggers.values().forEach(t -> t.currentGroup = this);
|
||||
|
||||
suites = ScriptLoader.getSerializer().toList(SceneSuite.class, bindings.get("suites"));
|
||||
regions = ScriptLoader.getSerializer().toList(SceneRegion.class, bindings.get("regions"));
|
||||
regions = ScriptLoader.getSerializer().toList(SceneRegion.class, bindings.get("regions")).stream()
|
||||
.collect(Collectors.toMap(x -> x.config_id, y -> y));
|
||||
regions.values().forEach(m -> m.group = this);
|
||||
|
||||
init_config = ScriptLoader.getSerializer().toObject(SceneInitConfig.class, bindings.get("init_config"));
|
||||
|
||||
// Garbages TODO fix properly later
|
||||
@ -157,6 +160,13 @@ public class SceneGroup {
|
||||
.map(triggers::get)
|
||||
.toList()
|
||||
);
|
||||
|
||||
suite.sceneRegions = new ArrayList<>(
|
||||
suite.regions.stream()
|
||||
.filter(regions::containsKey)
|
||||
.map(regions::get)
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
} catch (ScriptException e) {
|
||||
|
@ -1,62 +1,34 @@
|
||||
package emu.grasscutter.scripts.data;
|
||||
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.scripts.constants.ScriptRegionShape;
|
||||
import emu.grasscutter.utils.Position;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import lombok.Data;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
|
||||
@Setter
|
||||
public class SceneRegion {
|
||||
public int config_id;
|
||||
public int shape;
|
||||
public Position pos;
|
||||
// for CUBIC
|
||||
public Position size;
|
||||
// for SPHERE
|
||||
public int radius;
|
||||
|
||||
private boolean hasNewEntities;
|
||||
private final IntSet entities; // Ids of entities inside this region
|
||||
|
||||
public SceneRegion() {
|
||||
this.entities = new IntOpenHashSet();
|
||||
}
|
||||
|
||||
public IntSet getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
public void addEntity(GameEntity entity) {
|
||||
if (this.getEntities().contains(entity.getId())) {
|
||||
return;
|
||||
}
|
||||
this.getEntities().add(entity.getId());
|
||||
this.hasNewEntities = true;
|
||||
}
|
||||
|
||||
public void removeEntity(GameEntity entity) {
|
||||
this.getEntities().remove(entity.getId());
|
||||
}
|
||||
|
||||
public boolean contains(Position p) {
|
||||
public transient SceneGroup group;
|
||||
public boolean contains(Position position) {
|
||||
switch (shape) {
|
||||
case ScriptRegionShape.CUBIC:
|
||||
return (Math.abs(pos.getX() - p.getX()) <= size.getX()) &&
|
||||
(Math.abs(pos.getZ() - p.getZ()) <= size.getZ());
|
||||
return (Math.abs(pos.getX() - position.getX()) <= size.getX()) &&
|
||||
(Math.abs(pos.getY() - position.getY()) <= size.getY()) &&
|
||||
(Math.abs(pos.getZ() - position.getZ()) <= size.getZ());
|
||||
case ScriptRegionShape.SPHERE:
|
||||
return false;
|
||||
var x = Math.pow(pos.getX() - position.getX(), 2);
|
||||
var y = Math.pow(pos.getY() - position.getY(), 2);
|
||||
var z = Math.pow(pos.getZ() - position.getZ(), 2);
|
||||
return x + y + z <= (radius ^ 2);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasNewEntities() {
|
||||
return hasNewEntities;
|
||||
}
|
||||
|
||||
public void resetNewEntities() {
|
||||
hasNewEntities = false;
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,12 @@ public class SceneSuite {
|
||||
public List<Integer> monsters;
|
||||
public List<Integer> gadgets;
|
||||
public List<String> triggers;
|
||||
public int rand_weight;
|
||||
public List<Integer> regions;
|
||||
public int rand_weight;
|
||||
|
||||
public transient List<SceneMonster> sceneMonsters;
|
||||
public transient List<SceneGadget> sceneGadgets;
|
||||
public transient List<SceneTrigger> sceneTriggers;
|
||||
public transient List<SceneRegion> sceneRegions;
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user