blub
This commit is contained in:
parent
cabfdd4252
commit
fb77d13eb5
15 changed files with 447 additions and 3 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target/
|
||||||
35
src/main/java/de/welterde/em/Entity.java
Normal file
35
src/main/java/de/welterde/em/Entity.java
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public abstract class Entity {
|
||||||
|
protected final EntityStorage ctx;
|
||||||
|
protected final int id;
|
||||||
|
|
||||||
|
public Entity(EntityStorage ctx, int id) {
|
||||||
|
this.id = id;
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEntityId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/main/java/de/welterde/em/EntityRef.java
Normal file
67
src/main/java/de/welterde/em/EntityRef.java
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
* @param <X>
|
||||||
|
*/
|
||||||
|
public class EntityRef<X extends Entity> {
|
||||||
|
protected final EntityStorage ctx;
|
||||||
|
|
||||||
|
protected WeakReference<X> ref;
|
||||||
|
protected final int entityId;
|
||||||
|
protected boolean invalid;
|
||||||
|
|
||||||
|
public EntityRef(EntityStorage ctx, X entity) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.entityId = entity.getEntityId();
|
||||||
|
this.invalid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X get() {
|
||||||
|
if(this.invalid)
|
||||||
|
throw new InvalidEntityReference(this.entityId);
|
||||||
|
|
||||||
|
var e = this.ref.get();
|
||||||
|
if(e != null)
|
||||||
|
return e;
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
var ee = this.ctx.getEntity(this.entityId);
|
||||||
|
// check if entity was already destroyed
|
||||||
|
if (ee == null) {
|
||||||
|
this.invalid = true;
|
||||||
|
throw new InvalidEntityReference(this.entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to cast it to the correct type and since we do not reuse
|
||||||
|
// entity ids this should not go wrong, but maybe it did..
|
||||||
|
try {
|
||||||
|
X eee = (X) ee;
|
||||||
|
this.ref = new WeakReference(eee);
|
||||||
|
return eee;
|
||||||
|
} catch(ClassCastException exc) {
|
||||||
|
this.invalid = true;
|
||||||
|
throw new InvalidEntityReference(this.entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/de/welterde/em/EntityStorage.java
Normal file
25
src/main/java/de/welterde/em/EntityStorage.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public interface EntityStorage {
|
||||||
|
public Entity getEntity(int entityId);
|
||||||
|
}
|
||||||
41
src/main/java/de/welterde/em/InvalidEntityReference.java
Normal file
41
src/main/java/de/welterde/em/InvalidEntityReference.java
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public class InvalidEntityReference extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of <code>InvalidEntityReference</code> without
|
||||||
|
* detail message.
|
||||||
|
*/
|
||||||
|
public InvalidEntityReference() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance of <code>InvalidEntityReference</code> with the
|
||||||
|
* specified detail message.
|
||||||
|
*
|
||||||
|
* @param msg the detail message.
|
||||||
|
*/
|
||||||
|
public InvalidEntityReference(int eid) {
|
||||||
|
super("Invalid entity reference to entity=" + eid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,9 @@ package de.welterde.em.data;
|
||||||
* @author welterde
|
* @author welterde
|
||||||
*/
|
*/
|
||||||
public enum CounterName {
|
public enum CounterName {
|
||||||
|
COUNTRY_IDX,
|
||||||
|
DIMENSION_IDX,
|
||||||
|
AREA_IDX,
|
||||||
LOCATION_IDX,
|
LOCATION_IDX,
|
||||||
ENTITY_IDX,
|
ENTITY_IDX,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,12 @@ public interface Entity {
|
||||||
*/
|
*/
|
||||||
public Location getLocation();
|
public Location getLocation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative position within location
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public LocationPos getLocationPos();
|
||||||
|
|
||||||
public boolean isMovable();
|
public boolean isMovable();
|
||||||
|
|
||||||
public MovementStatus getMovementStatus();
|
public MovementStatus getMovementStatus();
|
||||||
|
|
|
||||||
25
src/main/java/de/welterde/em/data/LocationPos.java
Normal file
25
src/main/java/de/welterde/em/data/LocationPos.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em.data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public interface LocationPos {
|
||||||
|
|
||||||
|
}
|
||||||
44
src/main/java/de/welterde/em/w/Area.java
Normal file
44
src/main/java/de/welterde/em/w/Area.java
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
import de.welterde.em.data.Location;
|
||||||
|
import de.welterde.em.data.TerrainGen;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public class Area {
|
||||||
|
protected final int id;
|
||||||
|
protected final ReadWriteLock structLock;
|
||||||
|
|
||||||
|
protected final AreaMap map;
|
||||||
|
protected final Map<Integer, Location> locations;
|
||||||
|
|
||||||
|
public Area(int id, TerrainGen gen) {
|
||||||
|
this.id = id;
|
||||||
|
this.structLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
this.map = new AreaMap(gen);
|
||||||
|
this.locations = new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main/java/de/welterde/em/w/AreaMap.java
Normal file
49
src/main/java/de/welterde/em/w/AreaMap.java
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
import de.welterde.em.data.MapCoord;
|
||||||
|
import de.welterde.em.data.TerrainGen;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public class AreaMap {
|
||||||
|
protected final Map<MapCoord, Block> blockMap;
|
||||||
|
protected final TerrainGen gen;
|
||||||
|
protected final ReadWriteLock structLock;
|
||||||
|
|
||||||
|
public AreaMap(TerrainGen gen) {
|
||||||
|
this.gen = gen;
|
||||||
|
this.blockMap = new HashMap<>();
|
||||||
|
this.structLock = new ReentrantReadWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Block getBlock(MapCoord c) {
|
||||||
|
if(!this.blockMap.containsKey(c)) {
|
||||||
|
// default to the terrain generator
|
||||||
|
return this.gen.generateBlock(this, c);
|
||||||
|
} else {
|
||||||
|
return this.tiles.get(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/de/welterde/em/w/Block.java
Normal file
25
src/main/java/de/welterde/em/w/Block.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5m x 5m block of the world
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
public class Block {
|
||||||
|
|
||||||
|
}
|
||||||
25
src/main/java/de/welterde/em/w/BlockCoord.java
Normal file
25
src/main/java/de/welterde/em/w/BlockCoord.java
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2026 welterde
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author welterde
|
||||||
|
*/
|
||||||
|
class BlockCoord {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,10 +16,17 @@
|
||||||
*/
|
*/
|
||||||
package de.welterde.em.w;
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
import de.welterde.em.Entity;
|
||||||
|
import de.welterde.em.EntityStorage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author welterde
|
* @author welterde
|
||||||
*/
|
*/
|
||||||
public class Country {
|
public class Country extends Entity {
|
||||||
|
|
||||||
|
public Country(EntityStorage ctx, int id) {
|
||||||
|
super(ctx, id);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,39 @@
|
||||||
*/
|
*/
|
||||||
package de.welterde.em.w;
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
import de.welterde.em.Entity;
|
||||||
|
import de.welterde.em.data.CounterName;
|
||||||
|
import de.welterde.em.data.TerrainGen;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author welterde
|
* @author welterde
|
||||||
*/
|
*/
|
||||||
public class Dimension {
|
public class Dimension extends Entity {
|
||||||
|
protected final ReadWriteLock structLock;
|
||||||
|
|
||||||
|
protected final Map<Integer, Area> areas;
|
||||||
|
|
||||||
|
public Dimension(World ctx, int id) {
|
||||||
|
super(ctx, id);
|
||||||
|
this.structLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
this.areas = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Area addArea(TerrainGen gen) {
|
||||||
|
this.structLock.writeLock().lock();
|
||||||
|
try {
|
||||||
|
var areaId = this.ctx.allocateId(CounterName.AREA_IDX);
|
||||||
|
var area = new Area(areaId, gen);
|
||||||
|
this.areas.put(areaId, area);
|
||||||
|
return area;
|
||||||
|
} finally {
|
||||||
|
this.structLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,72 @@
|
||||||
*/
|
*/
|
||||||
package de.welterde.em.w;
|
package de.welterde.em.w;
|
||||||
|
|
||||||
|
import de.welterde.em.Entity;
|
||||||
|
import de.welterde.em.EntityRef;
|
||||||
|
import de.welterde.em.EntityStorage;
|
||||||
|
import de.welterde.em.data.CounterName;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author welterde
|
* @author welterde
|
||||||
*/
|
*/
|
||||||
public class World {
|
public class World implements EntityStorage {
|
||||||
|
protected final ArrayList<Entity> entities;
|
||||||
|
protected final EnumMap<CounterName, Integer> counters;
|
||||||
|
protected final ArrayList<EntityRef<Dimension>> dimensions;
|
||||||
|
protected final Map<Integer, EntityRef<Country>> countries;
|
||||||
|
protected final ReadWriteLock structLock;
|
||||||
|
|
||||||
|
public World() {
|
||||||
|
this.counters = new EnumMap<>(CounterName.class);
|
||||||
|
this.dimensions = new ArrayList<>();
|
||||||
|
this.countries = new HashMap<>();
|
||||||
|
this.entities = new ArrayList<>();
|
||||||
|
|
||||||
|
this.structLock = new ReentrantReadWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int allocateId(CounterName name) {
|
||||||
|
synchronized(this.counters) {
|
||||||
|
// start counter at zero if not yet a thing
|
||||||
|
if(!this.counters.containsKey(name))
|
||||||
|
this.counters.put(name, 0);
|
||||||
|
|
||||||
|
var nextVal = this.counters.get(name) + 1;
|
||||||
|
this.counters.put(name, nextVal);
|
||||||
|
return nextVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dimension addDimension() {
|
||||||
|
this.structLock.writeLock().lock();
|
||||||
|
try{
|
||||||
|
var dim = new Dimension(this, this.dimensions.size());
|
||||||
|
this.dimensions.add(dim);
|
||||||
|
return dim;
|
||||||
|
} finally {
|
||||||
|
this.structLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public Dimension getDimension(int id) {
|
||||||
|
this.structLock.readLock().lock();
|
||||||
|
try {
|
||||||
|
return this.dimensions.get(id);
|
||||||
|
} finally {
|
||||||
|
this.structLock.readLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entity getEntity(int entityId) {
|
||||||
|
return this.entities.get(entityId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue