Browse Source

load mesh data and structure meshes

server-collision
FatBoy-DOTC 11 months ago
parent
commit
adcf3fd9b6
  1. 7
      src/engine/CollisionEngine/CollisionManager.java
  2. 74
      src/engine/CollisionEngine/Mesh.java
  3. 4
      src/engine/CollisionEngine/Triangle.java
  4. 40
      src/engine/db/handlers/dbBuildingHandler.java
  5. 59
      src/engine/devcmd/cmds/ColliderCmd.java
  6. 5
      src/engine/devcmd/cmds/InfoCmd.java
  7. 12
      src/engine/devcmd/cmds/RegionCmd.java
  8. 121
      src/engine/gameManager/BuildingManager.java
  9. 1
      src/engine/gameManager/DevCmdManager.java
  10. 3
      src/engine/objects/Building.java
  11. 5
      src/engine/server/world/WorldServer.java

7
src/engine/CollisionEngine/CollisionManager.java

@ -6,17 +6,18 @@ import engine.objects.Building;
import java.awt.*; import java.awt.*;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
public class CollisionManager { public class CollisionManager {
public static HashMap<Integer, ArrayList<MeshData>> structure_meshes;
public static HashMap<Integer,ArrayList<Triangle>> mesh_triangles;
public static boolean CollisionDetected(Building building, Line2D travelLine, float charHeight, float charY){ public static boolean CollisionDetected(Building building, Line2D travelLine, float charHeight, float charY){
if(building.buildingRect != null) if(building.buildingRect != null)
if(!travelLine.intersects(building.buildingRect) && !building.buildingRect.contains(travelLine.getP1()) && !building.buildingRect.contains(travelLine.getP2())) if(!travelLine.intersects(building.buildingRect) && !building.buildingRect.contains(travelLine.getP1()) && !building.buildingRect.contains(travelLine.getP2()))
return false; return false;
for (Mesh mesh : building.buildingMeshes)
if(mesh.MeshCollides(travelLine,charHeight,charY))
return true;
return false; return false;
} }

74
src/engine/CollisionEngine/Mesh.java

@ -1,50 +1,62 @@
package engine.CollisionEngine; package engine.CollisionEngine;
import engine.math.Vector3f;
import engine.math.Vector3fImmutable;
import engine.objects.Building; import engine.objects.Building;
import org.pmw.tinylog.Logger;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.ArrayList; import java.util.ArrayList;
public class Mesh { public class Mesh {
public ArrayList<Triangle> triangles; public ArrayList<Triangle> triangles;
public ArrayList<Line2D> BoundingLines; public Vector3f mesh_end_point;
public Rectangle2D boundsRect; public Vector3f mesh_ref_point;
public float maxY; public Vector3f mesh_location;
public float minY; public float mesh_max_y;
public Building parentBuilding; public float mesh_min_y;
public Vector3f mesh_scale;
public boolean BoundsCollides(Line2D line){ public int mesh_id;
for(Line2D side : BoundingLines) public int parent_prop_id;
if(side.intersectsLine(line)) public int parent_structure_id;
return true; public int parentUUID;
public Rectangle2D.Float mesh_bounds;
return false;
} public void AdoptTriangles(float rotation){
if(CollisionManager.mesh_triangles.containsKey(this.mesh_id) == false){
public boolean MeshCollides(Line2D line, float charHeight, float charY){ Logger.error("Failed To Bake Triangles For Mesh: " + this.mesh_id);
return;
//check if movement path intersects this mesh
if(boundsRect == null){
return false;
} }
if(!line.intersects(boundsRect) && !boundsRect.contains(line.getP1()) && !boundsRect.contains(line.getP2())) this.triangles = new ArrayList<>();
return false; double radian = (double)rotation;
for(Triangle tri : CollisionManager.mesh_triangles.get(this.mesh_id)){
Triangle newTri = new Triangle();
Vector3f rotatedPoint1 = Vector3f.rotateAroundPoint(new Vector3f(tri.point1.x,mesh_location.y,tri.point1.y),mesh_location,radian);
Vector3f rotatedPoint2 = Vector3f.rotateAroundPoint(new Vector3f(tri.point2.x,mesh_location.y,tri.point2.y),mesh_location,radian);
Vector3f rotatedPoint3 = Vector3f.rotateAroundPoint(new Vector3f(tri.point3.x,mesh_location.y,tri.point3.y),mesh_location,radian);
//check to see if character is under or over the mesh newTri.point1 = new Point2D.Float(rotatedPoint1.x,rotatedPoint1.z);
float head = charY + charHeight; newTri.point2 = new Point2D.Float(rotatedPoint2.x,rotatedPoint2.z);
if(head < this.minY || charY > this.maxY) newTri.point3 = new Point2D.Float(rotatedPoint3.x,rotatedPoint3.z);
return false;
newTri.sides = new ArrayList<>();
newTri.sides.add(new Line2D.Float(newTri.point1,newTri.point2));
newTri.sides.add(new Line2D.Float(newTri.point2,newTri.point3));
newTri.sides.add(new Line2D.Float(newTri.point3,newTri.point1));
}
}
//check if any triangles intersect the movement path public void MakeBounds(float rotation){
for(Triangle tri : triangles) double radian = (double)rotation;
if(tri.collides(line)) Vector3f rotatedEnd = Vector3f.rotateAroundPoint(new Vector3f(this.mesh_end_point.x,mesh_location.y,this.mesh_end_point.z),mesh_location,radian);
return true; Vector3f rotatedRef = Vector3f.rotateAroundPoint(new Vector3f(this.mesh_ref_point.x,mesh_location.y,this.mesh_ref_point.z),mesh_location,radian);
//characters movement path did not intersect this mesh this.mesh_bounds = new Rectangle2D.Float();
return false; this.mesh_bounds.setRect(rotatedEnd.x,rotatedEnd.z,Math.abs(rotatedEnd.x) - Math.abs(rotatedRef.x),Math.abs(rotatedEnd.z) - Math.abs(rotatedRef.z));
} }
} }

4
src/engine/CollisionEngine/Triangle.java

@ -2,9 +2,13 @@ package engine.CollisionEngine;
import java.awt.geom.Line2D; import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList; import java.util.ArrayList;
public class Triangle { public class Triangle {
public Point2D.Float point1;
public Point2D.Float point2;
public Point2D.Float point3;
public ArrayList<Line2D> sides; public ArrayList<Line2D> sides;
public boolean collides(Line2D line) public boolean collides(Line2D line)
{ {

40
src/engine/db/handlers/dbBuildingHandler.java

@ -9,7 +9,9 @@
package engine.db.handlers; package engine.db.handlers;
import engine.CollisionEngine.CollisionManager;
import engine.CollisionEngine.MeshData; import engine.CollisionEngine.MeshData;
import engine.CollisionEngine.Triangle;
import engine.Enum; import engine.Enum;
import engine.Enum.DbObjectType; import engine.Enum.DbObjectType;
import engine.Enum.ProtectionState; import engine.Enum.ProtectionState;
@ -23,6 +25,8 @@ import engine.objects.*;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.pmw.tinylog.Logger; import org.pmw.tinylog.Logger;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
@ -855,11 +859,12 @@ public class dbBuildingHandler extends dbHandlerBase {
} }
public void LOAD_MESH_DATA(){ public void LOAD_MESH_DATA(){
CollisionManager.structure_meshes = new HashMap<>();
try (Connection connection = DbManager.getConnection(); try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `final_structure_meshes`")) { PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `final_structure_meshes`")) {
ResultSet rs = preparedStatement.executeQuery(); ResultSet rs = preparedStatement.executeQuery();
ArrayList<MeshData> meshData = new ArrayList<>();
while (rs.next()) { while (rs.next()) {
MeshData md = new MeshData(); MeshData md = new MeshData();
md.propID = rs.getInt("propID"); md.propID = rs.getInt("propID");
@ -870,9 +875,38 @@ public class dbBuildingHandler extends dbHandlerBase {
md.endPoint = new Vector3f(rs.getFloat("endX"), rs.getFloat("endY"),rs.getFloat("endZ")); md.endPoint = new Vector3f(rs.getFloat("endX"), rs.getFloat("endY"),rs.getFloat("endZ"));
md.minY = rs.getFloat("minY"); md.minY = rs.getFloat("minY");
md.maxY = rs.getFloat("maxY"); md.maxY = rs.getFloat("maxY");
meshData.add(md); if(CollisionManager.structure_meshes.containsKey(rs.getInt("propID"))){
CollisionManager.structure_meshes.get(rs.getInt("propID")).add(md);
} else{
ArrayList<MeshData> meshData = new ArrayList<>();
meshData.add(md);
CollisionManager.structure_meshes.put(rs.getInt("propID"),meshData);
}
}
} catch (SQLException e) {
Logger.error(e);
}
}
public void LOAD_MESH_TRIANGLE_DATA(){
CollisionManager.mesh_triangles = new HashMap<>();
try (Connection connection = DbManager.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM `final_mesh_triangles`")) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
Triangle tri = new Triangle();
tri.point1 = new Point2D.Float(rs.getFloat("P1X"),rs.getFloat("P1Z"));
tri.point2 = new Point2D.Float(rs.getFloat("P2X"),rs.getFloat("P2Z"));
tri.point3 = new Point2D.Float(rs.getFloat("P3X"),rs.getFloat("P3Z"));
if(CollisionManager.mesh_triangles.containsKey(rs.getInt("meshID"))){
CollisionManager.mesh_triangles.get(rs.getInt("meshID")).add(tri);
} else{
ArrayList<Triangle> triData = new ArrayList<>();
triData.add(tri);
CollisionManager.mesh_triangles.put(rs.getInt("meshID"),triData);
}
} }
int i = 0; //finished;
} catch (SQLException e) { } catch (SQLException e) {
Logger.error(e); Logger.error(e);
} }

59
src/engine/devcmd/cmds/ColliderCmd.java

@ -0,0 +1,59 @@
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2022
// www.magicbane.com
package engine.devcmd.cmds;
import engine.CollisionEngine.Mesh;
import engine.Enum;
import engine.devcmd.AbstractDevCmd;
import engine.objects.*;
public class ColliderCmd extends AbstractDevCmd {
public ColliderCmd() {
super("collider");
}
@Override
protected void _doCmd(PlayerCharacter pc, String[] words,
AbstractGameObject target) {
String newline = "\r\n ";
String output;
if(target.getObjectType().equals(Enum.GameObjectType.Building) == false){
throwbackInfo(pc,"Please Select A Building To Show Collider Data");
}
Building building = (Building)target;
output = "Collision Info:" + newline;
output += "Total Meshes: " + building.buildingMeshes.size() + newline;
for(Mesh mesh : building.buildingMeshes){
output += "-----------------------------";
output += "Mesh ID: " + mesh.mesh_id + newline;
output += "Mesh Location: " + mesh.mesh_location + newline;
output += "Mesh Min/Max: " + mesh.mesh_min_y + "/" + mesh.mesh_max_y + newline;
output += "Mesh Triangle Count: " + mesh.triangles.size() + newline;
output += "Mesh Rect: " + mesh.mesh_bounds + newline;
output += "-----------------------------";
}
throwbackInfo(pc,output);
}
@Override
protected String _getHelpString() {
return "Displays Information About Colliders";
}
@Override
protected String _getUsageString() {
return "' /collider displays collision info when selected on a building";
}
}

5
src/engine/devcmd/cmds/InfoCmd.java

@ -245,11 +245,6 @@ public class InfoCmd extends AbstractDevCmd {
for (Regions regions : targetBuilding.getBounds().getRegions()) { for (Regions regions : targetBuilding.getBounds().getRegions()) {
//TODO ADD REGION INFO //TODO ADD REGION INFO
} }
output += "-------Mesh Data-------" + newline;
output += "Mesh Count: " + targetBuilding.buildingMeshes.size() + newline;
for(Mesh mesh : targetBuilding.buildingMeshes){
output += "Mesh Rect: " + mesh.boundsRect + newline;
}
break; break;
case PlayerCharacter: case PlayerCharacter:
output += newline; output += newline;

12
src/engine/devcmd/cmds/RegionCmd.java

@ -47,18 +47,6 @@ public class RegionCmd extends AbstractDevCmd {
output += "is Outside: " + region.isOutside(); output += "is Outside: " + region.isOutside();
this.throwbackInfo(pc, output); this.throwbackInfo(pc, output);
} }
if(building != null){
this.throwbackInfo(pc, "Building Rect: " + building.buildingRect);
for (Mesh mesh : building.buildingMeshes){
//this.throwbackInfo(pc, "Mesh Rect: " + mesh.boundsRect);
if(mesh.boundsRect.contains(pc.loc.x,pc.loc.z)) {
this.throwbackInfo(pc, "Inside A Mesh's Bounds");
this.throwbackInfo(pc, "Rect: " + mesh.boundsRect);
return;
}
}
}
this.throwbackInfo(pc, "Outside All Mesh Bounds");
} }
@Override @Override

121
src/engine/gameManager/BuildingManager.java

@ -9,6 +9,8 @@
package engine.gameManager; package engine.gameManager;
import engine.CollisionEngine.CollisionManager;
import engine.CollisionEngine.MeshData;
import engine.Enum; import engine.Enum;
import engine.Enum.BuildingGroup; import engine.Enum.BuildingGroup;
import engine.Enum.GameObjectType; import engine.Enum.GameObjectType;
@ -956,8 +958,8 @@ public enum BuildingManager {
cleanupHirelings(building); cleanupHirelings(building);
//rebake colliders for change in rank //rebake colliders for change in rank
BuildingManager.BakeBuildingMeshes(building); //BuildingManager.BakeBuildingMeshes(building);
BuildingManager.BakeBuildingColliders(building);
building.isDeranking.compareAndSet(true, false); building.isDeranking.compareAndSet(true, false);
} }
@ -976,99 +978,32 @@ public enum BuildingManager {
return null; return null;
} }
public static void BakeBuildingMeshes(Building building){ public static void BakeBuildingColliders(Building building){
try {
if (building == null)
return;
if(building.meshUUID == 450700){
int i = 0; // stop point for testing
}
building.buildingMeshes = new ArrayList<>();
//float rotation = building.getRot().getRotation();
double radian = building.getBounds().getQuaternion().angleY;
int degrees = (int) Math.toDegrees(radian);
Vector3f buildingLoc = new Vector3f(building.loc.x, building.loc.y, building.loc.z);
if(prop_meshes.containsKey(building.meshUUID) == false)
return;//no meshes to load for this prop
ArrayList<Integer> meshes = prop_meshes.get(building.meshUUID);
for (int mesh : meshes) {
if (!mesh_heights.containsKey(mesh)) {
continue; //no data for this mesh
}
if(!mesh_triangle_points.containsKey(mesh))
continue;
Mesh generatedMesh = new Mesh();
generatedMesh.parentBuilding = building;
generatedMesh.maxY = building.loc.y + mesh_heights.get(mesh).x;
generatedMesh.minY = building.loc.y + mesh_heights.get(mesh).y;
ArrayList<ArrayList<Vector3f>> triPoints = mesh_triangle_points.get(mesh);
if (mesh_bounding_boxes.containsKey(mesh)) {
Rectangle2D boundingBox = mesh_bounding_boxes.get(mesh);
float halfX = (float) boundingBox.getWidth() * 0.5f;
float halfZ = (float) boundingBox.getHeight() * 0.5f;
Vector3f bottomRight = new Vector3f(building.loc.x + halfX, building.loc.y, building.loc.z + halfZ);
Vector3f topLeft = new Vector3f(building.loc.x - halfX, building.loc.y, building.loc.z - halfZ);
Vector3f topRight = new Vector3f(building.loc.x + halfX, building.loc.y, building.loc.z - halfZ);
Vector3f bottomLeft = new Vector3f(building.loc.x - halfX, building.loc.y, building.loc.z + halfZ);
generatedMesh.BoundingLines = new ArrayList<>();
Point2D.Float p1 = new Point2D.Float(topLeft.x, topLeft.z);
Point2D.Float p2 = new Point2D.Float(topRight.x, topRight.z);
Point2D.Float p3 = new Point2D.Float(bottomRight.x, bottomRight.z);
Point2D.Float p4 = new Point2D.Float(bottomLeft.x, bottomLeft.z);
float rectWidth = topLeft.distance(topRight);
float rectHeight = topLeft.distance(bottomLeft);
generatedMesh.boundsRect = new Rectangle2D.Float();
generatedMesh.boundsRect.setRect(p1.x, p1.y, rectWidth, rectHeight);
generatedMesh.BoundingLines.add(new Line2D.Float(p1, p2));
generatedMesh.BoundingLines.add(new Line2D.Float(p2, p3));
generatedMesh.BoundingLines.add(new Line2D.Float(p3, p4));
generatedMesh.BoundingLines.add(new Line2D.Float(p4, p1));
//generatedMesh.boundsRect = boundingBox;
} else{
Logger.error("No Bounding Box Data For Mesh: " + mesh);
}
generatedMesh.triangles = new ArrayList<>();
for (ArrayList<Vector3f> pointList : triPoints) {
ArrayList<Vector3f> rotatedPoints = new ArrayList<>();
for (Vector3f point : pointList) {
Vector3f calculatedOffsetPoint = buildingLoc.add(point);
rotatedPoints.add(Vector3f.rotateAroundPoint(buildingLoc, calculatedOffsetPoint, radian));
}
Point2D.Float p1 = new Point2D.Float(rotatedPoints.get(0).x, rotatedPoints.get(0).z);
Point2D.Float p2 = new Point2D.Float(rotatedPoints.get(1).x, rotatedPoints.get(1).z);
Point2D.Float p3 = new Point2D.Float(rotatedPoints.get(2).x, rotatedPoints.get(2).z);
Triangle tri = new Triangle(); if(CollisionManager.structure_meshes.containsKey(building.meshUUID) == false) {
tri.sides = new ArrayList<>(); Logger.error("No Meshes Found Fro Structure: " + building.meshUUID);
tri.sides.add(new Line2D.Float(p1, p2)); return;
tri.sides.add(new Line2D.Float(p2, p3)); }
tri.sides.add(new Line2D.Float(p3, p1));
generatedMesh.triangles.add(tri);
} //create the empty array of meshes
building.buildingMeshes.add(generatedMesh); building.buildingMeshes = new ArrayList<>();
}
//Rectangle2D.Float buildingBound = new Rectangle2D.Float(); //create the actual meshes from the stored mesh data
float xLoc = building.loc.x - building.getBounds().getHalfExtents().x; for(MeshData meshData : CollisionManager.structure_meshes.get(building.meshUUID)){
float zLoc = building.loc.z - building.getBounds().getHalfExtents().y; Mesh generatedMesh = new Mesh();
float width = building.getBounds().getHalfExtents().x * 2; generatedMesh.mesh_end_point = new Vector3f(building.loc.x,building.loc.y,building.loc.z).add(meshData.loc).add(meshData.endPoint);
float height = building.getBounds().getHalfExtents().y * 2; generatedMesh.mesh_ref_point = new Vector3f(building.loc.x,building.loc.y,building.loc.z).add(meshData.loc).add(meshData.refPoint);
building.buildingRect = new Rectangle2D.Float(); generatedMesh.mesh_location = new Vector3f(building.loc.x,building.loc.y,building.loc.z).add(meshData.loc);
building.buildingRect.setRect(xLoc,zLoc,width,height); generatedMesh.mesh_max_y = building.loc.y + meshData.maxY;
//building.buildingRect = buildingBound; generatedMesh.mesh_min_y = building.loc.y + meshData.minY;
} generatedMesh.mesh_scale = meshData.scale;
catch(Exception e){ generatedMesh.mesh_id = meshData.meshID;
Logger.info("Failed To Bake Building Mesh Data For Structure: " + building.meshUUID); generatedMesh.parent_prop_id = meshData.propID;
generatedMesh.parent_structure_id = building.meshUUID;
generatedMesh.parentUUID = building.getObjectUUID();
generatedMesh.AdoptTriangles(building.getBounds().getQuaternion().angleY);
generatedMesh.MakeBounds(building.getBounds().getQuaternion().angleY);
building.buildingMeshes.add(generatedMesh);
} }
} }
} }

1
src/engine/gameManager/DevCmdManager.java

@ -140,6 +140,7 @@ public enum DevCmdManager {
DevCmdManager.registerDevCmd(new BoundsCmd()); DevCmdManager.registerDevCmd(new BoundsCmd());
DevCmdManager.registerDevCmd(new GotoBoundsCmd()); DevCmdManager.registerDevCmd(new GotoBoundsCmd());
DevCmdManager.registerDevCmd(new RegionCmd()); DevCmdManager.registerDevCmd(new RegionCmd());
DevCmdManager.registerDevCmd(new ColliderCmd());
DevCmdManager.registerDevCmd(new SetMaintCmd()); DevCmdManager.registerDevCmd(new SetMaintCmd());
DevCmdManager.registerDevCmd(new ApplyBonusCmd()); DevCmdManager.registerDevCmd(new ApplyBonusCmd());
DevCmdManager.registerDevCmd(new AuditFailedItemsCmd()); DevCmdManager.registerDevCmd(new AuditFailedItemsCmd());

3
src/engine/objects/Building.java

@ -1010,7 +1010,8 @@ public class Building extends AbstractWorldObject {
if (this.upgradeDateTime != null) if (this.upgradeDateTime != null)
BuildingManager.submitUpgradeJob(this); BuildingManager.submitUpgradeJob(this);
BuildingManager.BakeBuildingMeshes(this); //BuildingManager.BakeBuildingMeshes(this);
BuildingManager.BakeBuildingColliders(this);
} }
public synchronized boolean setOwner(AbstractCharacter newOwner) { public synchronized boolean setOwner(AbstractCharacter newOwner) {

5
src/engine/server/world/WorldServer.java

@ -310,9 +310,10 @@ public class WorldServer {
Guild.getErrantGuild(); Guild.getErrantGuild();
Logger.info("Loading Server Collision Meshes."); Logger.info("Loading Server Collision Meshes.");
DbManager.BuildingQueries.LOAD_PROP_MESHES(); //DbManager.BuildingQueries.LOAD_PROP_MESHES();
DbManager.BuildingQueries.LOAD_MESH_DATA(); DbManager.BuildingQueries.LOAD_MESH_DATA();
DbManager.BuildingQueries.LOAD_MESH_BOUNDING_BOXES(); DbManager.BuildingQueries.LOAD_MESH_TRIANGLE_DATA();
//DbManager.BuildingQueries.LOAD_MESH_BOUNDING_BOXES();
Logger.info("Loading zone template data"); Logger.info("Loading zone template data");
DbManager.ZoneQueries.LOAD_ALL_ZONE_TEMPLATES(); DbManager.ZoneQueries.LOAD_ALL_ZONE_TEMPLATES();

Loading…
Cancel
Save