From 833ca720f16dec3e0d6698116816a8f5c7c79892 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 17 Jul 2022 16:24:41 +0200 Subject: [PATCH] Added ability to render mutiple players. --- client/pom.xml | 5 + .../java/nl/andrewl/aos2_client/Client.java | 44 +- .../nl/andrewl/aos2_client/ClientWorld.java | 131 ++++ .../PlayerInputMouseClickCallback.java | 1 - .../aos2_client/render/GameRenderer.java | 117 ++-- .../nl/andrewl/aos2_client/render/Mesh.java | 70 -- .../andrewl/aos2_client/render/MeshData.java | 6 - .../aos2_client/render/ModelRenderer.java | 53 ++ .../render/PlayerMeshGenerator.java | 26 - .../aos2_client/render/ShaderProgram.java | 4 + .../aos2_client/render/chunk/ChunkMesh.java | 17 +- .../render/chunk/ChunkRenderer.java | 47 +- .../aos2_client/render/gui/GUIRenderer.java | 20 +- .../aos2_client/render/gui/GUITexture.java | 2 +- .../aos2_client/render/model/Model.java | 123 ++++ .../main/resources/model/player_simple.mtl | 13 + .../main/resources/model/player_simple.obj | 604 ++++++++++++++++++ .../main/resources/model/simple_player.png | Bin 0 -> 65324 bytes .../main/resources/shader/model/fragment.glsl | 20 + .../main/resources/shader/model/vertex.glsl | 18 + .../java/nl/andrewl/aos_core/ImageUtils.java | 12 +- .../java/nl/andrewl/aos_core/model/Chunk.java | 6 +- .../aos_core/net/PlayerJoinMessage.java | 8 + .../nl/andrewl/aos_core/net/UdpReceiver.java | 1 - .../aos_core/net/udp/ChunkUpdateMessage.java | 8 + .../aos_core/net/udp/PlayerUpdateMessage.java | 10 +- .../ClientCommunicationHandler.java | 7 + 27 files changed, 1133 insertions(+), 240 deletions(-) delete mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java delete mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/ModelRenderer.java delete mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/model/Model.java create mode 100644 client/src/main/resources/model/player_simple.mtl create mode 100644 client/src/main/resources/model/player_simple.obj create mode 100644 client/src/main/resources/model/simple_player.png create mode 100644 client/src/main/resources/shader/model/fragment.glsl create mode 100644 client/src/main/resources/shader/model/vertex.glsl diff --git a/client/pom.xml b/client/pom.xml index 6912333..6fc7531 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -146,6 +146,11 @@ aos2-core ${parent.version} + + de.javagl + obj + 0.3.0 + org.lwjgl diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index 57b06cf..3815a84 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -5,16 +5,11 @@ import nl.andrewl.aos2_client.control.PlayerInputKeyCallback; import nl.andrewl.aos2_client.control.PlayerInputMouseClickCallback; import nl.andrewl.aos2_client.control.PlayerViewCursorCallback; import nl.andrewl.aos2_client.render.GameRenderer; -import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.ColorPalette; -import nl.andrewl.aos_core.model.World; -import nl.andrewl.aos_core.net.ChunkDataMessage; -import nl.andrewl.aos_core.net.ChunkHashMessage; -import nl.andrewl.aos_core.net.WorldInfoMessage; +import nl.andrewl.aos_core.net.*; import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.record_net.Message; -import org.joml.Vector3i; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,14 +55,18 @@ public class Client implements Runnable { gameRenderer.setupWindow( new PlayerViewCursorCallback(gameRenderer.getCamera(), communicationHandler), new PlayerInputKeyCallback(inputHandler), - new PlayerInputMouseClickCallback(inputHandler) + new PlayerInputMouseClickCallback(inputHandler), + false, + false ); long lastFrameAt = System.currentTimeMillis(); while (!gameRenderer.windowShouldClose()) { long now = System.currentTimeMillis(); float dt = (now - lastFrameAt) / 1000f; + world.processQueuedChunkUpdates(); gameRenderer.getCamera().interpolatePosition(dt); + world.interpolatePlayers(dt); gameRenderer.draw(); lastFrameAt = now; } @@ -75,31 +74,17 @@ public class Client implements Runnable { communicationHandler.shutdown(); } - public int getClientId() { - return clientId; - } - - public World getWorld() { - return world; - } - public void onMessageReceived(Message msg) { if (msg instanceof WorldInfoMessage worldInfo) { world.setPalette(ColorPalette.fromArray(worldInfo.palette())); } if (msg instanceof ChunkDataMessage chunkDataMessage) { - Chunk chunk = chunkDataMessage.toChunk(); - world.addChunk(chunk); - gameRenderer.getChunkRenderer().queueChunkMesh(chunk); + world.addChunk(chunkDataMessage); } if (msg instanceof ChunkUpdateMessage u) { - Vector3i chunkPos = new Vector3i(u.cx(), u.cy(), u.cz()); - Chunk chunk = world.getChunkAt(chunkPos); - System.out.println(u); - if (chunk != null) { - chunk.setBlockAt(u.lx(), u.ly(), u.lz(), u.newBlock()); - gameRenderer.getChunkRenderer().queueChunkMesh(chunk); - } else { + world.updateChunk(u); + // If we received an update for a chunk we don't have, request it! + if (world.getChunkAt(u.getChunkPos()) == null) { communicationHandler.sendMessage(new ChunkHashMessage(u.cx(), u.cy(), u.cz(), -1)); } } @@ -108,9 +93,16 @@ public class Client implements Runnable { float eyeHeight = playerUpdate.crouching() ? 1.3f : 1.7f; gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + eyeHeight, playerUpdate.pz()); gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz()); - // TODO: Unload far away chunks and request close chunks we don't have. + } else { + world.playerUpdated(playerUpdate); } } + if (msg instanceof PlayerJoinMessage joinMessage) { + world.playerJoined(joinMessage); + } + if (msg instanceof PlayerLeaveMessage leaveMessage) { + world.playerLeft(leaveMessage); + } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java b/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java index b434ada..b163577 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java +++ b/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java @@ -1,7 +1,138 @@ package nl.andrewl.aos2_client; +import nl.andrewl.aos2_client.render.chunk.ChunkMesh; +import nl.andrewl.aos2_client.render.chunk.ChunkMeshGenerator; +import nl.andrewl.aos_core.model.Chunk; +import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.World; +import nl.andrewl.aos_core.net.ChunkDataMessage; +import nl.andrewl.aos_core.net.PlayerJoinMessage; +import nl.andrewl.aos_core.net.PlayerLeaveMessage; +import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; +import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; +import org.joml.Vector3f; +import org.joml.Vector3i; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * A client-side extension of the world model, with information that is only + * important for the client to know about, like other players and chunk render + * queues. + */ public class ClientWorld extends World { + private final Queue chunkUpdateQueue = new ConcurrentLinkedQueue<>(); + private final Queue chunkRemovalQueue = new ConcurrentLinkedQueue<>(); + private final ChunkMeshGenerator chunkMeshGenerator = new ChunkMeshGenerator(); + private final Map chunkMeshes = new ConcurrentHashMap<>(); + private final Map players = new HashMap<>(); + + public void playerJoined(PlayerJoinMessage joinMessage) { + Player p = joinMessage.toPlayer(); + players.put(p.getId(), p); + } + + public void playerLeft(PlayerLeaveMessage leaveMessage) { + players.remove(leaveMessage.id()); + } + + public void playerUpdated(PlayerUpdateMessage playerUpdate) { + Player p = players.get(playerUpdate.clientId()); + if (p != null) { + playerUpdate.apply(p); + } + } + + public Collection getPlayers() { + return players.values(); + } + + public void interpolatePlayers(float dt) { + Vector3f movement = new Vector3f(); + for (var player : getPlayers()) { + movement.set(player.getVelocity()).mul(dt); + player.getPosition().add(movement); + } + } + + @Override + public void addChunk(Chunk chunk) { + super.addChunk(chunk); + chunkUpdateQueue.add(chunk); + } + + public void addChunk(ChunkDataMessage msg) { + addChunk(msg.toChunk()); + } + + @Override + public void removeChunk(Vector3i chunkPos) { + Chunk chunk = getChunkAt(chunkPos); + if (chunk != null) { + chunkRemovalQueue.add(chunk); + chunkUpdateQueue.remove(chunk); + } + super.removeChunk(chunkPos); + } + + public void updateChunk(ChunkUpdateMessage update) { + Chunk chunk = getChunkAt(update.getChunkPos()); + if (chunk != null) { + chunk.setBlockAt(update.lx(), update.ly(), update.lz(), update.newBlock()); + List chunksToReRender = new ArrayList<>(7); + chunksToReRender.add(chunk); + // Check if neighboring chunks need to be re-rendered too. + if (update.lx() == 0) { + Chunk c = getChunkAt(update.cx() - 1, update.cy(), update.cz()); + if (c != null) chunksToReRender.add(c); + } + if (update.ly() == 0) { + Chunk c = getChunkAt(update.cx(), update.cy() - 1, update.cz()); + if (c != null) chunksToReRender.add(c); + } + if (update.lz() == 0) { + Chunk c = getChunkAt(update.cx(), update.cy(), update.cz() - 1); + if (c != null) chunksToReRender.add(c); + } + if (update.lx() == Chunk.SIZE - 1) { + Chunk c = getChunkAt(update.cx() + 1, update.cy(), update.cz()); + if (c != null) chunksToReRender.add(c); + } + if (update.ly() == Chunk.SIZE - 1) { + Chunk c = getChunkAt(update.cx(), update.cy() + 1, update.cz()); + if (c != null) chunksToReRender.add(c); + } + if (update.lz() == Chunk.SIZE - 1) { + Chunk c = getChunkAt(update.cx(), update.cy(), update.cz() + 1); + if (c != null) chunksToReRender.add(c); + } + chunkUpdateQueue.addAll(chunksToReRender); + } + } + + /** + * Call this to process any queued chunk updates, and update chunk meshes. + * Only call this method on the main OpenGL context thread! + */ + public void processQueuedChunkUpdates() { + while (!chunkRemovalQueue.isEmpty()) { + Chunk chunk = chunkRemovalQueue.remove(); + ChunkMesh mesh = chunkMeshes.remove(chunk); + if (mesh != null) mesh.free(); + } + while (!chunkUpdateQueue.isEmpty()) { + Chunk chunk = chunkUpdateQueue.remove(); + ChunkMesh mesh = new ChunkMesh(chunk, this, chunkMeshGenerator); + ChunkMesh existingMesh = chunkMeshes.get(chunk); + if (existingMesh != null) existingMesh.free(); + chunkMeshes.put(chunk, mesh); + } + } + + public Collection getChunkMeshesToDraw() { + return chunkMeshes.values(); + } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java index b4c63fe..7f8a4f6 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java @@ -14,7 +14,6 @@ public class PlayerInputMouseClickCallback implements GLFWMouseButtonCallbackI { @Override public void invoke(long window, int button, int action, int mods) { - System.out.println("Click: " + button); inputHandler.updateInputState(window); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java index 58d5924..a66f5b1 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java @@ -1,10 +1,11 @@ package nl.andrewl.aos2_client.render; import nl.andrewl.aos2_client.Camera; +import nl.andrewl.aos2_client.ClientWorld; import nl.andrewl.aos2_client.render.chunk.ChunkRenderer; import nl.andrewl.aos2_client.render.gui.GUIRenderer; import nl.andrewl.aos2_client.render.gui.GUITexture; -import nl.andrewl.aos_core.model.World; +import nl.andrewl.aos2_client.render.model.Model; import org.joml.Matrix4f; import org.lwjgl.glfw.*; import org.lwjgl.opengl.GL; @@ -27,30 +28,33 @@ public class GameRenderer { private static final float Z_NEAR = 0.01f; private static final float Z_FAR = 500f; - private final ChunkRenderer chunkRenderer; - private final GUIRenderer guiRenderer; + private ChunkRenderer chunkRenderer; + private GUIRenderer guiRenderer; + private ModelRenderer modelRenderer; private final Camera camera; - private final World world; + private final ClientWorld world; + private Model playerModel; // Standard player model used to render all players. private long windowHandle; - private GLFWVidMode primaryMonitorSettings; - private boolean fullscreen; private int screenWidth = 800; private int screenHeight = 600; private float fov = 90f; private final Matrix4f perspectiveTransform; - public GameRenderer(World world) { + public GameRenderer(ClientWorld world) { this.world = world; - this.chunkRenderer = new ChunkRenderer(); - this.guiRenderer = new GUIRenderer(); this.camera = new Camera(); this.perspectiveTransform = new Matrix4f(); - } - public void setupWindow(GLFWCursorPosCallbackI viewCursorCallback, GLFWKeyCallbackI inputKeyCallback, GLFWMouseButtonCallbackI mouseButtonCallback) { + public void setupWindow( + GLFWCursorPosCallbackI viewCursorCallback, + GLFWKeyCallbackI inputKeyCallback, + GLFWMouseButtonCallbackI mouseButtonCallback, + boolean fullscreen, + boolean grabCursor + ) { GLFWErrorCallback.createPrint(System.err).set(); if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); glfwDefaultWindowHints(); @@ -58,21 +62,28 @@ public class GameRenderer { glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); long monitorId = glfwGetPrimaryMonitor(); - primaryMonitorSettings = glfwGetVideoMode(monitorId); + GLFWVidMode primaryMonitorSettings = glfwGetVideoMode(monitorId); if (primaryMonitorSettings == null) throw new IllegalStateException("Could not get information about the primary monitory."); log.debug("Primary monitor settings: Width: {}, Height: {}", primaryMonitorSettings.width(), primaryMonitorSettings.height()); - screenWidth = primaryMonitorSettings.width(); - screenHeight = primaryMonitorSettings.height(); - windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", monitorId, 0); + if (fullscreen) { + screenWidth = primaryMonitorSettings.width(); + screenHeight = primaryMonitorSettings.height(); + windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", monitorId, 0); + } else { + screenWidth = 1000; + screenHeight = 800; + windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", 0, 0); + } if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); - fullscreen = true; log.debug("Initialized GLFW window."); // Setup callbacks. glfwSetKeyCallback(windowHandle, inputKeyCallback); glfwSetCursorPosCallback(windowHandle, viewCursorCallback); glfwSetMouseButtonCallback(windowHandle, mouseButtonCallback); - glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + if (grabCursor) { + glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); glfwSetCursorPos(windowHandle, 0, 0); log.debug("Set up window callbacks."); @@ -80,7 +91,6 @@ public class GameRenderer { glfwMakeContextCurrent(windowHandle); glfwSwapInterval(1); glfwShowWindow(windowHandle); - log.debug("Made window visible."); GL.createCapabilities(); // GLUtil.setupDebugMessageCallback(System.out); @@ -90,9 +100,9 @@ public class GameRenderer { glCullFace(GL_BACK); log.debug("Initialized OpenGL context."); - chunkRenderer.setupShaderProgram(); + this.chunkRenderer = new ChunkRenderer(); log.debug("Initialized chunk renderer."); - guiRenderer.setup(); + this.guiRenderer = new GUIRenderer(); // TODO: More organized way to load textures for GUI. try { var crosshairTexture = new GUITexture("gui/crosshair.png"); @@ -103,34 +113,13 @@ public class GameRenderer { throw new RuntimeException(e); } log.debug("Initialized GUI renderer."); - updatePerspective(); - } - - public void setFullscreen(boolean fullscreen) { - if (windowHandle == 0) throw new IllegalStateException("Window not setup."); - long monitor = glfwGetPrimaryMonitor(); - if (fullscreen) { - log.debug("Changing to fullscreen: {} x {}", primaryMonitorSettings.width(), primaryMonitorSettings.height()); - glfwSetWindowMonitor(windowHandle, monitor, 0, 0, primaryMonitorSettings.width(), primaryMonitorSettings.height(), primaryMonitorSettings.refreshRate()); - screenWidth = primaryMonitorSettings.width(); - screenHeight = primaryMonitorSettings.height(); - updatePerspective(); - } else { - log.debug("Changing to windowed mode."); - screenWidth = 800; - screenHeight = 600; - int left = primaryMonitorSettings.width() / 2; - int top = primaryMonitorSettings.height() / 2; - glfwSetWindowMonitor(windowHandle, 0, left, top, screenWidth, screenHeight, primaryMonitorSettings.refreshRate()); - updatePerspective(); + this.modelRenderer = new ModelRenderer(); + try { + playerModel = new Model("model/player_simple.obj", "model/simple_player.png"); + } catch (IOException e) { + throw new RuntimeException(e); } - this.fullscreen = fullscreen; - } - - public void setSize(int width, int height) { - glfwSetWindowSize(windowHandle, width, height); - this.screenWidth = width; - this.screenHeight = height; + log.debug("Initialized model renderer."); updatePerspective(); } @@ -144,12 +133,18 @@ public class GameRenderer { } /** - * Updates the rendering perspective used to render the game. Note: only - * call this after calling {@link ChunkRenderer#setupShaderProgram()}. + * Updates the rendering perspective used to render the game. */ private void updatePerspective() { perspectiveTransform.setPerspective(fov, getAspectRatio(), Z_NEAR, Z_FAR); - chunkRenderer.setPerspective(perspectiveTransform); + float[] data = new float[16]; + perspectiveTransform.get(data); + if (chunkRenderer != null) chunkRenderer.setPerspective(data); + if (modelRenderer != null) modelRenderer.setPerspective(data); + } + + public Matrix4f getPerspectiveTransform() { + return perspectiveTransform; } public boolean windowShouldClose() { @@ -160,14 +155,20 @@ public class GameRenderer { return camera; } - public ChunkRenderer getChunkRenderer() { - return chunkRenderer; - } - public void draw() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + chunkRenderer.draw(camera, world.getChunkMeshesToDraw()); + + // Draw players. + modelRenderer.setView(camera.getViewTransformData()); + playerModel.bind(); + Matrix4f playerModelTransform = new Matrix4f(); + for (var player : world.getPlayers()) { + playerModelTransform.identity().translate(player.getPosition()); + modelRenderer.render(playerModel, playerModelTransform); + } + playerModel.unbind(); - chunkRenderer.draw(camera, world); guiRenderer.draw(); glfwSwapBuffers(windowHandle); @@ -175,8 +176,10 @@ public class GameRenderer { } public void freeWindow() { - guiRenderer.free(); - chunkRenderer.free(); + if (playerModel != null) playerModel.free(); + if (modelRenderer != null) modelRenderer.free(); + if (guiRenderer != null) guiRenderer.free(); + if (chunkRenderer != null) chunkRenderer.free(); GL.destroy(); Callbacks.glfwFreeCallbacks(windowHandle); glfwSetErrorCallback(null); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java deleted file mode 100644 index e815948..0000000 --- a/client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java +++ /dev/null @@ -1,70 +0,0 @@ -package nl.andrewl.aos2_client.render; - -import org.joml.Matrix4f; - -import static org.lwjgl.opengl.GL46.*; - -public class Mesh { - private final int vboId; - private final int vaoId; - private final int eboId; - private int indexCount; - private final Matrix4f transform = new Matrix4f(); - private final float[] transformData = new float[16]; - - public Mesh(MeshData initialData) { - this.vboId = glGenBuffers(); - this.eboId = glGenBuffers(); - this.vaoId = glGenVertexArrays(); - load(initialData); - initVertexArrayAttributes(); - } - - public void load(MeshData data) { - indexCount = data.indexBuffer().limit(); - glBindBuffer(GL_ARRAY_BUFFER, vboId); - glBufferData(GL_ARRAY_BUFFER, data.vertexBuffer(), GL_STATIC_DRAW); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, data.indexBuffer(), GL_STATIC_DRAW); - } - - public Matrix4f getTransform() { - return transform; - } - - public void updateTransform() { - transform.set(transformData); - } - - public float[] getTransformData() { - return transformData; - } - - /** - * Initializes this mesh's vertex array attribute settings. - */ - private void initVertexArrayAttributes() { - glBindVertexArray(vaoId); - // Vertex position floats. - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, false, 9 * Float.BYTES, 0); - // Vertex color floats. - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, false, 9 * Float.BYTES, 3 * Float.BYTES); - // Vertex normal floats. - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 3, GL_FLOAT, false, 9 * Float.BYTES, 6 * Float.BYTES); - } - - public void draw() { - glBindVertexArray(vaoId); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); - glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0); - } - - public void free() { - glDeleteBuffers(vboId); - glDeleteBuffers(eboId); - glDeleteVertexArrays(vaoId); - } -} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java b/client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java deleted file mode 100644 index fde20be..0000000 --- a/client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java +++ /dev/null @@ -1,6 +0,0 @@ -package nl.andrewl.aos2_client.render; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; - -public record MeshData(FloatBuffer vertexBuffer, IntBuffer indexBuffer) {} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ModelRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/ModelRenderer.java new file mode 100644 index 0000000..3c26344 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ModelRenderer.java @@ -0,0 +1,53 @@ +package nl.andrewl.aos2_client.render; + +import nl.andrewl.aos2_client.Camera; +import nl.andrewl.aos2_client.render.model.Model; +import org.joml.Matrix4f; + +import static org.lwjgl.opengl.GL46.*; + +/** + * A renderer that handles rendering of textured models. + */ +public class ModelRenderer { + private final ShaderProgram shaderProgram; + private final int projectionUniform; + private final int viewUniform; + private final int modelUniform; + private final int textureSamplerUniform; + + public ModelRenderer() { + shaderProgram = new ShaderProgram.Builder() + .withShader("shader/model/vertex.glsl", GL_VERTEX_SHADER) + .withShader("shader/model/fragment.glsl", GL_FRAGMENT_SHADER) + .build(); + projectionUniform = shaderProgram.getUniform("projectionTransform"); + viewUniform = shaderProgram.getUniform("viewTransform"); + modelUniform = shaderProgram.getUniform("modelTransform"); + textureSamplerUniform = shaderProgram.getUniform("textureSampler"); + } + + public void setPerspective(float[] data) { + shaderProgram.use(); + glUniformMatrix4fv(projectionUniform, false, data); + shaderProgram.stopUsing(); + } + + public void setView(float[] data) { + shaderProgram.use(); + glUniformMatrix4fv(viewUniform, false, data); + shaderProgram.stopUsing(); + } + + public void render(Model model, Matrix4f modelTransform) { + shaderProgram.use(); + glUniformMatrix4fv(modelUniform, false, modelTransform.get(new float[16])); + glUniform1i(textureSamplerUniform, 0); + model.draw(); + shaderProgram.stopUsing(); + } + + public void free() { + shaderProgram.free(); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java b/client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java deleted file mode 100644 index db831ca..0000000 --- a/client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java +++ /dev/null @@ -1,26 +0,0 @@ -package nl.andrewl.aos2_client.render; - -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.lwjgl.BufferUtils; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; - -public class PlayerMeshGenerator { - private final FloatBuffer vertexBuffer; - private final IntBuffer indexBuffer; - - private final Vector3i pos = new Vector3i(); - private final Vector3f color = new Vector3f(); - private final Vector3f norm = new Vector3f(); - - public PlayerMeshGenerator() { - vertexBuffer = BufferUtils.createFloatBuffer(1000); - indexBuffer = BufferUtils.createIntBuffer(100); - } - -// public PlayerMesh generateMesh() { -// -// } -} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ShaderProgram.java b/client/src/main/java/nl/andrewl/aos2_client/render/ShaderProgram.java index 7b1f30e..2557509 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/ShaderProgram.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ShaderProgram.java @@ -32,6 +32,10 @@ public class ShaderProgram { glUseProgram(0); } + public int getId() { + return id; + } + public int getUniform(String name) { return glGetUniformLocation(id, name); } diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkMesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkMesh.java index 4192bc9..5363b32 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkMesh.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkMesh.java @@ -49,17 +49,16 @@ public class ChunkMesh { * Generates and loads this chunk's mesh into the allocated OpenGL buffers. */ private void loadMesh(ChunkMeshGenerator meshGenerator) { - long start = System.nanoTime(); +// long start = System.nanoTime(); var meshData = meshGenerator.generateMesh(chunk, world); - double dur = (System.nanoTime() - start) / 1_000_000.0; +// double dur = (System.nanoTime() - start) / 1_000_000.0; this.indexCount = meshData.indexBuffer().limit(); - // Print some debug information. - log.debug( - "Generated mesh for chunk ({}, {}, {}) in {} ms. {} vertices and {} indices.", - chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z, - dur, - meshData.vertexBuffer().limit() / 9, indexCount - ); +// log.debug( +// "Generated mesh for chunk ({}, {}, {}) in {} ms. {} vertices and {} indices.", +// chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z, +// dur, +// meshData.vertexBuffer().limit() / 9, indexCount +// ); glBindBuffer(GL_ARRAY_BUFFER, vboId); glBufferData(GL_ARRAY_BUFFER, meshData.vertexBuffer(), GL_STATIC_DRAW); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkRenderer.java index db3b697..0b8184e 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/chunk/ChunkRenderer.java @@ -7,7 +7,10 @@ import nl.andrewl.aos_core.model.World; import org.joml.Matrix4f; import org.joml.Vector3i; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import static org.lwjgl.opengl.GL46.*; @@ -18,17 +21,12 @@ import static org.lwjgl.opengl.GL46.*; * be rendered each frame. */ public class ChunkRenderer { - private final ChunkMeshGenerator chunkMeshGenerator = new ChunkMeshGenerator(); - private final Queue meshGenerationQueue = new ConcurrentLinkedQueue<>(); + private final ShaderProgram shaderProgram; + private final int projectionTransformUniform; + private final int viewTransformUniform; + private final int chunkPositionUniform; - private ShaderProgram shaderProgram; - private int projectionTransformUniform; - private int viewTransformUniform; - private int chunkPositionUniform; - - private final Map chunkMeshes = new HashMap<>(); - - public void setupShaderProgram() { + public ChunkRenderer() { this.shaderProgram = new ShaderProgram.Builder() .withShader("shader/chunk/vertex.glsl", GL_VERTEX_SHADER) .withShader("shader/chunk/fragment.glsl", GL_FRAGMENT_SHADER) @@ -38,39 +36,28 @@ public class ChunkRenderer { this.viewTransformUniform = shaderProgram.getUniform("viewTransform"); this.chunkPositionUniform = shaderProgram.getUniform("chunkPosition"); int chunkSizeUniform = shaderProgram.getUniform("chunkSize"); - // Set constant uniforms that don't change during runtime. glUniform1i(chunkSizeUniform, Chunk.SIZE); + shaderProgram.stopUsing(); } - public void queueChunkMesh(Chunk chunk) { - meshGenerationQueue.add(chunk); + public void setPerspective(float[] data) { + shaderProgram.use(); + glUniformMatrix4fv(projectionTransformUniform, false, data); + shaderProgram.stopUsing(); } - public void setPerspective(Matrix4f projectionTransform) { - glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16])); - } - - public void draw(Camera cam, World world) { - while (!meshGenerationQueue.isEmpty()) { - Chunk chunk = meshGenerationQueue.remove(); - ChunkMesh mesh = new ChunkMesh(chunk, world, chunkMeshGenerator); - ChunkMesh existingMesh = chunkMeshes.get(chunk.getPosition()); - if (existingMesh != null) existingMesh.free(); - chunkMeshes.put(chunk.getPosition(), mesh); - } + public void draw(Camera cam, Collection chunkMeshes) { shaderProgram.use(); glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData()); - for (var mesh : chunkMeshes.values()) { + for (var mesh : chunkMeshes) { glUniform3iv(chunkPositionUniform, mesh.getPositionData()); mesh.draw(); } + shaderProgram.stopUsing(); } public void free() { - for (var mesh : chunkMeshes.values()) mesh.free(); - chunkMeshes.clear(); - meshGenerationQueue.clear(); shaderProgram.free(); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUIRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUIRenderer.java index a263a4c..ba6dc99 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUIRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUIRenderer.java @@ -14,20 +14,16 @@ import static org.lwjgl.opengl.GL46.*; * Manages rendering of 2D GUI components like cross-hairs, inventory stuff, etc. */ public class GUIRenderer { - private int vaoId; - private int vboId; - private int vertexCount; - private ShaderProgram shaderProgram; - private int transformUniformLocation; + private final int vaoId; + private final int vboId; + private final int vertexCount; + private final ShaderProgram shaderProgram; + private final int transformUniformLocation; private final List guiTextures = new ArrayList<>(); - public void addTexture(GUITexture texture) { - guiTextures.add(texture); - } - - public void setup() { + public GUIRenderer() { vaoId = glGenVertexArrays(); vboId = glGenBuffers(); FloatBuffer buffer = BufferUtils.createFloatBuffer(8); @@ -53,6 +49,10 @@ public class GUIRenderer { shaderProgram.bindAttribute(0, "position"); } + public void addTexture(GUITexture texture) { + guiTextures.add(texture); + } + public void draw() { shaderProgram.use(); glBindVertexArray(vaoId); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUITexture.java b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUITexture.java index 4cc345d..59d15ce 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUITexture.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/gui/GUITexture.java @@ -26,7 +26,7 @@ public class GUITexture { textureId = glGenTextures(); glBindTexture(GL_TEXTURE_2D, textureId); - glPixelStorei(GL_UNPACK_ALIGNMENT, textureId); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); var buf = ImageUtils.decodePng(img); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/model/Model.java b/client/src/main/java/nl/andrewl/aos2_client/render/model/Model.java new file mode 100644 index 0000000..205b76a --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/model/Model.java @@ -0,0 +1,123 @@ +package nl.andrewl.aos2_client.render.model; + +import de.javagl.obj.Obj; +import de.javagl.obj.ObjData; +import de.javagl.obj.ObjReader; +import de.javagl.obj.ObjUtils; +import nl.andrewl.aos_core.ImageUtils; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.lwjgl.opengl.GL46.*; + +/** + * Represents a 3D model with a texture, which can be used to render one or + * more entities. + */ +public class Model { + private final int vaoId; + private final List vboIds; + private final int eboId; + private final int indexCount; + private final int textureId; + + public Model(String resource, String textureResource) throws IOException { + try ( + var in = Model.class.getClassLoader().getResourceAsStream(resource); + var imageIn = Model.class.getClassLoader().getResourceAsStream(textureResource) + ) { + if (in == null) throw new IOException("Could not load resource: " + resource); + if (imageIn == null) throw new IOException("Could not load texture image: " + textureResource); + Obj obj = ObjReader.read(in); + obj = ObjUtils.convertToRenderable(obj); + IntBuffer indices = ObjData.getFaceVertexIndices(obj, 3); + FloatBuffer vertices = ObjData.getVertices(obj); + FloatBuffer texCoords = ObjData.getTexCoords(obj, 2, true); + FloatBuffer normals = ObjData.getNormals(obj); + indexCount = indices.limit(); + + vboIds = new ArrayList<>(4); + + vaoId = glGenVertexArrays(); + glBindVertexArray(vaoId); + + // Position data + int vboId = glGenBuffers(); + vboIds.add(vboId); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0); + + // Normal data + vboId = glGenBuffers(); + vboIds.add(vboId); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + glBufferData(GL_ARRAY_BUFFER, normals, GL_STATIC_DRAW); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0); + + // Texture data + vboId = glGenBuffers(); + vboIds.add(vboId); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + glBufferData(GL_ARRAY_BUFFER, texCoords, GL_STATIC_DRAW); + glEnableVertexAttribArray(2); // Texture coordinates + glVertexAttribPointer(2, 2, GL_FLOAT, false, 0, 0); + + // Index data + eboId = glGenBuffers(); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW); + + textureId = glGenTextures(); + glBindTexture(GL_TEXTURE_2D, textureId); + BufferedImage img = ImageIO.read(imageIn); + int w = img.getWidth(); + int h = img.getHeight(); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ByteBuffer imageBuffer = ImageUtils.decodePng(img); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageBuffer); + } + } + + public void bind() { + glBindVertexArray(vaoId); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glEnableVertexAttribArray(2); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textureId); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); + } + + public void draw() { + glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0); + } + + public void unbind() { + glBindVertexArray(0); + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + glBindTexture(GL_TEXTURE_2D, 0); + } + + public void free() { + glDeleteTextures(textureId); + glDeleteBuffers(eboId); + for (var vboId : vboIds) { + glDeleteBuffers(vboId); + } + glDeleteVertexArrays(vaoId); + } +} diff --git a/client/src/main/resources/model/player_simple.mtl b/client/src/main/resources/model/player_simple.mtl new file mode 100644 index 0000000..8f219e3 --- /dev/null +++ b/client/src/main/resources/model/player_simple.mtl @@ -0,0 +1,13 @@ +# Blender MTL File: 'player_simple.blend' +# Material Count: 1 + +newmtl Material.001 +Ns 225.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.800000 0.800000 0.800000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 +map_Kd simple_player.png diff --git a/client/src/main/resources/model/player_simple.obj b/client/src/main/resources/model/player_simple.obj new file mode 100644 index 0000000..f232464 --- /dev/null +++ b/client/src/main/resources/model/player_simple.obj @@ -0,0 +1,604 @@ +# Blender v2.82 (sub 7) OBJ File: 'player_simple.blend' +# www.blender.org +mtllib player_simple.mtl +o Cube +v 0.307055 1.384430 -0.307055 +v 0.159554 0.004036 -0.159554 +v 0.307055 1.384430 0.307055 +v 0.159554 0.004036 0.159554 +v -0.307055 1.384430 -0.307055 +v -0.159554 0.004036 -0.159554 +v -0.307055 1.384430 0.307055 +v -0.159554 0.004036 0.159554 +v 0.000000 0.004036 -0.221565 +v 0.000000 1.384430 0.426394 +v 0.000000 0.004036 0.221565 +v 0.000000 1.384430 -0.426394 +v -0.221565 0.004036 0.000000 +v 0.426394 1.384430 0.000000 +v -0.426394 1.384430 0.000000 +v 0.221565 0.004036 0.000000 +v 0.000000 0.004036 0.000000 +v 0.125637 1.521320 -0.125637 +v 0.355691 0.694233 -0.355691 +v -0.355691 0.694233 0.355691 +v 0.355691 0.694233 0.355691 +v -0.355691 0.694233 -0.355691 +v 0.000000 0.694233 -0.493931 +v 0.000000 0.694233 0.493931 +v 0.493931 0.694233 -0.000000 +v -0.493931 0.694233 -0.000000 +v 0.342552 0.349135 -0.342552 +v 0.342552 0.349135 0.342552 +v -0.342552 0.349135 -0.342552 +v 0.000000 0.349135 -0.475686 +v 0.000000 0.349135 0.475686 +v 0.475686 0.349135 -0.000000 +v -0.475686 0.349135 -0.000000 +v -0.342552 0.349135 0.342552 +v -0.342552 1.039332 0.342552 +v 0.342552 1.039332 -0.342552 +v 0.342552 1.039332 0.342552 +v -0.342552 1.039332 -0.342552 +v 0.000000 1.039332 -0.475686 +v 0.000000 1.039332 0.475686 +v 0.475686 1.039332 -0.000000 +v -0.475686 1.039332 -0.000000 +v 0.324796 0.176514 -0.324796 +v 0.324796 0.176514 0.324796 +v -0.324796 0.176514 -0.324796 +v 0.000000 0.176514 -0.451029 +v 0.000000 0.176514 0.451029 +v 0.451029 0.176514 -0.000000 +v -0.451029 0.176514 -0.000000 +v -0.324796 0.176514 0.324796 +v 0.274970 0.090275 -0.274970 +v 0.274970 0.090275 0.274970 +v -0.274970 0.090275 -0.274970 +v 0.000000 0.090275 -0.381838 +v 0.000000 0.090275 0.381838 +v 0.381838 0.090275 -0.000000 +v -0.381838 0.090275 -0.000000 +v -0.274970 0.090275 0.274970 +v 0.230376 1.470570 -0.230376 +v 0.230376 1.470570 0.230376 +v -0.230376 1.470570 -0.230376 +v -0.230376 1.470570 0.230376 +v 0.000000 1.470570 0.319913 +v 0.000000 1.470570 -0.319913 +v 0.319913 1.470570 0.000000 +v -0.319913 1.470570 0.000000 +v 0.125637 1.556870 -0.125637 +v 0.125637 1.521320 0.125637 +v -0.125637 1.521320 -0.125637 +v -0.125637 1.521320 0.125637 +v 0.000000 1.521320 0.174467 +v 0.000000 1.521320 -0.174467 +v 0.174467 1.521320 0.000000 +v -0.174467 1.521320 0.000000 +v 0.169463 1.610223 -0.169463 +v 0.125637 1.556870 0.125637 +v -0.125637 1.556870 -0.125637 +v -0.125637 1.556870 0.125637 +v 0.000000 1.556870 0.174467 +v 0.000000 1.556870 -0.174467 +v 0.174467 1.556870 0.000000 +v -0.174467 1.556870 0.000000 +v 0.210794 1.686314 -0.210794 +v 0.169463 1.610223 0.169463 +v -0.169463 1.610223 -0.169463 +v -0.169463 1.610223 0.169463 +v 0.000000 1.610223 0.235325 +v 0.000000 1.610223 -0.235325 +v 0.235325 1.610223 0.000000 +v -0.235325 1.610223 0.000000 +v 0.223284 1.833569 -0.223284 +v 0.210794 1.686314 0.210794 +v -0.210794 1.686314 -0.210794 +v -0.210794 1.686314 0.210794 +v 0.000000 1.686314 0.292720 +v 0.000000 1.686314 -0.292720 +v 0.292720 1.686314 0.000000 +v -0.292720 1.686314 0.000000 +v 0.211578 1.977176 -0.211578 +v 0.223284 1.833569 0.223284 +v -0.223284 1.833569 -0.223284 +v -0.223284 1.833569 0.223284 +v 0.000000 1.833569 0.310064 +v 0.000000 1.833569 -0.310064 +v 0.310064 1.833569 0.000000 +v -0.310064 1.833569 0.000000 +v 0.172806 2.077383 -0.172806 +v 0.211578 1.977176 0.211578 +v -0.211578 1.977176 -0.211578 +v -0.211578 1.977176 0.211578 +v 0.000000 1.977176 0.293809 +v 0.000000 1.977176 -0.293809 +v 0.293809 1.977176 0.000000 +v -0.293809 1.977176 0.000000 +v 0.067534 2.131719 -0.067534 +v 0.172806 2.077383 0.172806 +v -0.172806 2.077383 -0.172806 +v -0.172806 2.077383 0.172806 +v 0.000000 2.077383 0.239968 +v 0.000000 2.077383 -0.239968 +v 0.239968 2.077383 0.000000 +v -0.239968 2.077383 0.000000 +v 0.067534 2.131719 0.067534 +v -0.067534 2.131719 -0.067534 +v -0.067534 2.131719 0.067534 +v 0.000000 2.131719 0.093781 +v 0.000000 2.131719 -0.093781 +v 0.093781 2.131719 0.000000 +v -0.093781 2.131719 0.000000 +v 0.000000 2.131719 0.000000 +vt 0.270204 0.781152 +vt 0.118734 0.781152 +vt 0.138824 0.736777 +vt 0.252468 0.736777 +vt 0.757867 0.465320 +vt 0.690242 0.473338 +vt 0.690321 0.364703 +vt 0.757955 0.344126 +vt 0.757867 0.142172 +vt 0.690242 0.150190 +vt 0.690321 0.041556 +vt 0.757955 0.020979 +vt 0.210691 0.204882 +vt 0.247595 0.112545 +vt 0.305645 0.164231 +vt 0.305645 0.240769 +vt 0.499666 0.142172 +vt 0.432041 0.150190 +vt 0.432120 0.041555 +vt 0.499754 0.020979 +vt 0.499666 0.465320 +vt 0.432041 0.473338 +vt 0.432120 0.364703 +vt 0.499754 0.344126 +vt 0.499754 0.629641 +vt 0.432120 0.620631 +vt 0.173786 0.297218 +vt 0.252494 0.297218 +vt 0.757955 0.629641 +vt 0.690321 0.620631 +vt 0.016446 0.672518 +vt 0.016446 0.525224 +vt 0.062080 0.544760 +vt 0.062080 0.655271 +vt 0.381919 0.534391 +vt 0.381919 0.681685 +vt 0.336285 0.662149 +vt 0.336285 0.551638 +vt 0.115736 0.245532 +vt 0.115736 0.168995 +vt 0.499754 0.306493 +vt 0.432120 0.297484 +vt 0.168888 0.112545 +vt 0.757955 0.306493 +vt 0.690321 0.297484 +vt 0.893270 0.306425 +vt 0.825604 0.309807 +vt 0.825512 0.139183 +vt 0.893182 0.142104 +vt 0.635069 0.306425 +vt 0.567403 0.309807 +vt 0.567311 0.139183 +vt 0.634981 0.142104 +vt 0.893270 0.629572 +vt 0.825604 0.632954 +vt 0.825512 0.462330 +vt 0.893182 0.465251 +vt 0.635069 0.629572 +vt 0.567403 0.632954 +vt 0.567311 0.462330 +vt 0.634981 0.465251 +vt 0.567403 0.336488 +vt 0.635069 0.344058 +vt 0.567403 0.013341 +vt 0.635069 0.020910 +vt 0.825604 0.013341 +vt 0.893270 0.020910 +vt 0.825604 0.336488 +vt 0.893270 0.344058 +vt 0.927041 0.469228 +vt 0.927124 0.354316 +vt 0.927041 0.146080 +vt 0.927124 0.031169 +vt 0.668840 0.146080 +vt 0.668923 0.031169 +vt 0.668840 0.469228 +vt 0.668923 0.354316 +vt 0.668923 0.625032 +vt 0.927124 0.625032 +vt 0.668923 0.301884 +vt 0.927124 0.301884 +vt 0.282733 0.364011 +vt 0.295787 0.392847 +vt 0.135566 0.392847 +vt 0.147091 0.364011 +vt 0.138649 0.045752 +vt 0.125594 0.016917 +vt 0.285815 0.016917 +vt 0.274291 0.045752 +vt 0.374332 0.134826 +vt 0.403985 0.122132 +vt 0.403985 0.277935 +vt 0.374332 0.266728 +vt 0.047049 0.274937 +vt 0.017396 0.287632 +vt 0.017397 0.131828 +vt 0.047049 0.143035 +vt 0.145897 0.470132 +vt 0.259541 0.470132 +vt 0.232099 0.530746 +vt 0.170123 0.530746 +vt 0.124413 0.631713 +vt 0.166266 0.676163 +vt 0.228242 0.676163 +vt 0.273952 0.635464 +vt 0.124413 0.571445 +vt 0.128161 0.425756 +vt 0.279631 0.425756 +vt 0.405682 0.414476 +vt 0.405650 0.458926 +vt 0.398680 0.458930 +vt 0.398712 0.414480 +vt 0.405682 0.528622 +vt 0.405650 0.573072 +vt 0.398680 0.573076 +vt 0.398712 0.528626 +vt 0.405682 0.519194 +vt 0.398712 0.519197 +vt 0.405682 0.633340 +vt 0.398712 0.633344 +vt 0.273952 0.575196 +vt 0.907386 0.807523 +vt 0.907386 0.747255 +vt 0.933468 0.736089 +vt 0.933468 0.817380 +vt 0.799699 0.706556 +vt 0.757846 0.751006 +vt 0.731765 0.741148 +vt 0.788217 0.681193 +vt 0.757846 0.811274 +vt 0.803557 0.851973 +vt 0.793420 0.877335 +vt 0.731765 0.822439 +vt 0.865533 0.851973 +vt 0.877016 0.877335 +vt 0.413282 0.458926 +vt 0.413314 0.519194 +vt 0.406344 0.519197 +vt 0.406312 0.458930 +vt 0.421034 0.458926 +vt 0.421066 0.519194 +vt 0.414097 0.519197 +vt 0.414065 0.458930 +vt 0.421066 0.414476 +vt 0.414097 0.414480 +vt 0.413314 0.414476 +vt 0.406344 0.414480 +vt 0.783860 0.901254 +vt 0.707168 0.832969 +vt 0.887844 0.901254 +vt 0.707168 0.731852 +vt 0.871813 0.681193 +vt 0.881373 0.657274 +vt 0.958065 0.725559 +vt 0.861676 0.706556 +vt 0.059593 0.887311 +vt 0.059648 0.988429 +vt 0.030770 0.991625 +vt 0.030712 0.884517 +vt 0.997431 0.015588 +vt 0.997377 0.090166 +vt 0.968496 0.087372 +vt 0.968554 0.008375 +vt 0.997431 0.421298 +vt 0.997377 0.495876 +vt 0.968496 0.493081 +vt 0.968554 0.414084 +vt 0.997431 0.191283 +vt 0.968554 0.194480 +vt 0.777388 0.657274 +vt 0.958065 0.826676 +vt 0.940352 0.495728 +vt 0.940407 0.420873 +vt 0.940407 0.191512 +vt 0.940352 0.090019 +vt 0.968554 0.600190 +vt 0.940407 0.597221 +vt 0.030770 0.805520 +vt 0.002569 0.887164 +vt 0.002623 0.812308 +vt 0.997377 0.293021 +vt 0.997431 0.394138 +vt 0.968554 0.397335 +vt 0.968496 0.290226 +vt 0.997431 0.218443 +vt 0.968554 0.211229 +vt 0.059648 0.812733 +vt 0.997431 0.596993 +vt 0.503196 0.657315 +vt 0.607567 0.657315 +vt 0.598599 0.679753 +vt 0.513354 0.679753 +vt 0.684545 0.725854 +vt 0.684545 0.827347 +vt 0.661471 0.818627 +vt 0.661471 0.735732 +vt 0.614063 0.902203 +vt 0.603905 0.879765 +vt 0.940407 0.015163 +vt 0.002623 0.988657 +vt 0.940407 0.394367 +vt 0.940352 0.292873 +vt 0.940407 0.218018 +vt 0.576323 0.818842 +vt 0.598820 0.794949 +vt 0.598820 0.762553 +vt 0.574250 0.740676 +vt 0.455788 0.823786 +vt 0.455788 0.740891 +vt 0.518438 0.764569 +vt 0.518438 0.796965 +vt 0.518660 0.879765 +vt 0.543009 0.818842 +vt 0.509692 0.902203 +vt 0.432714 0.833664 +vt 0.432714 0.732171 +vt 0.558629 0.779759 +vt 0.540936 0.740676 +vn -0.6110 0.7552 0.2374 +vn -0.3591 0.1320 0.9239 +vn -0.9239 0.1320 -0.3591 +vn 0.0000 -1.0000 0.0000 +vn 0.9239 0.1320 0.3591 +vn 0.3591 0.1320 -0.9239 +vn -0.3591 0.1320 -0.9239 +vn 0.3591 0.1320 0.9239 +vn 0.2374 0.7552 0.6110 +vn -0.2374 0.7552 -0.6110 +vn 0.9239 0.1320 -0.3591 +vn -0.9239 0.1320 0.3591 +vn -0.2374 0.7552 0.6110 +vn -0.9309 -0.0492 0.3618 +vn 0.9309 -0.0492 -0.3618 +vn 0.3618 -0.0492 0.9309 +vn -0.3618 -0.0492 -0.9309 +vn 0.3618 -0.0492 -0.9309 +vn 0.9309 -0.0492 0.3618 +vn -0.9309 -0.0492 -0.3618 +vn -0.3618 -0.0492 0.9309 +vn -0.3591 -0.1320 0.9239 +vn -0.9239 -0.1320 -0.3591 +vn 0.9239 -0.1320 0.3591 +vn 0.3591 -0.1320 -0.9239 +vn -0.3591 -0.1320 -0.9239 +vn 0.3591 -0.1320 0.9239 +vn 0.9239 -0.1320 -0.3591 +vn -0.9239 -0.1320 0.3591 +vn -0.9309 0.0492 0.3618 +vn 0.9309 0.0492 -0.3618 +vn 0.3618 0.0492 0.9309 +vn -0.3618 0.0492 -0.9309 +vn 0.3618 0.0492 -0.9309 +vn 0.9309 0.0492 0.3618 +vn -0.9309 0.0492 -0.3618 +vn -0.3618 0.0492 0.9309 +vn -0.7464 -0.5989 0.2901 +vn 0.7464 -0.5989 -0.2901 +vn 0.2901 -0.5989 0.7464 +vn -0.2901 -0.5989 -0.7464 +vn 0.2901 -0.5989 -0.7464 +vn 0.7464 -0.5989 0.2901 +vn -0.7464 -0.5989 -0.2901 +vn -0.2901 -0.5989 0.7464 +vn -0.1811 -0.8660 0.4660 +vn -0.4660 -0.8660 -0.1811 +vn 0.4660 -0.8660 0.1811 +vn 0.1811 -0.8660 -0.4660 +vn -0.1811 -0.8660 -0.4660 +vn 0.1811 -0.8660 0.4660 +vn 0.4660 -0.8660 -0.1811 +vn -0.4660 -0.8660 0.1811 +vn 0.3268 0.9365 -0.1270 +vn -0.1270 0.9365 0.3268 +vn -0.3268 0.9365 -0.1270 +vn 0.1270 0.9365 0.3268 +vn -0.6110 0.7552 -0.2374 +vn 0.6110 0.7552 0.2374 +vn 0.6110 0.7552 -0.2374 +vn 0.2374 0.7552 -0.6110 +vn -0.3623 0.0000 0.9321 +vn -0.9321 0.0000 -0.3623 +vn 0.3623 0.0000 0.9321 +vn -0.9321 0.0000 0.3623 +vn -0.1270 0.9365 -0.3268 +vn 0.1270 0.9365 -0.3268 +vn 0.3268 0.9365 0.1270 +vn -0.3268 0.9365 0.1270 +vn -0.6386 -0.7284 0.2482 +vn 0.6386 -0.7284 0.2482 +vn 0.2482 -0.7284 -0.6386 +vn -0.2482 -0.7284 -0.6386 +vn 0.9321 0.0000 -0.3623 +vn -0.3623 0.0000 -0.9321 +vn 0.3623 0.0000 -0.9321 +vn 0.9321 0.0000 0.3623 +vn 0.2963 -0.5751 -0.7625 +vn -0.2963 -0.5751 -0.7625 +vn 0.7625 -0.5751 -0.2963 +vn -0.2963 -0.5751 0.7625 +vn 0.2482 -0.7284 0.6386 +vn -0.6386 -0.7284 -0.2482 +vn -0.2482 -0.7284 0.6386 +vn 0.6386 -0.7284 -0.2482 +vn 0.9265 -0.1091 -0.3601 +vn -0.3601 -0.1091 0.9265 +vn -0.9265 -0.1091 -0.3601 +vn 0.3601 -0.1091 0.9265 +vn 0.7625 -0.5751 0.2963 +vn -0.7625 -0.5751 0.2963 +vn 0.2963 -0.5751 0.7625 +vn -0.7625 -0.5751 -0.2963 +vn -0.9269 0.1049 -0.3603 +vn 0.3603 0.1049 0.9269 +vn -0.9269 0.1049 0.3603 +vn 0.9269 0.1049 0.3603 +vn -0.3601 -0.1091 -0.9265 +vn 0.3601 -0.1091 -0.9265 +vn 0.9265 -0.1091 0.3601 +vn -0.9265 -0.1091 0.3601 +vn 0.8334 0.4478 0.3239 +vn 0.3239 0.4478 -0.8334 +vn -0.3239 0.4478 -0.8334 +vn 0.8334 0.4478 -0.3239 +vn -0.3603 0.1049 0.9269 +vn 0.9269 0.1049 -0.3603 +vn -0.3603 0.1049 -0.9269 +vn 0.3603 0.1049 -0.9269 +vn -0.1342 0.9289 -0.3452 +vn 0.3452 0.9289 -0.1342 +vn -0.1342 0.9289 0.3452 +vn -0.3452 0.9289 -0.1342 +vn -0.8334 0.4478 0.3239 +vn 0.3239 0.4478 0.8334 +vn -0.8334 0.4478 -0.3239 +vn -0.3239 0.4478 0.8334 +vn 0.0000 1.0000 0.0000 +vn 0.1342 0.9289 -0.3452 +vn 0.3452 0.9289 0.1342 +vn -0.3452 0.9289 0.1342 +vn 0.1342 0.9289 0.3452 +usemtl Material.001 +s off +f 15/1/1 7/2/1 62/3/1 66/4/1 +f 40/5/2 10/6/2 7/7/2 35/8/2 +f 42/9/3 15/10/3 5/11/3 38/12/3 +f 17/13/4 16/14/4 4/15/4 11/16/4 +f 41/17/5 14/18/5 3/19/5 37/20/5 +f 39/21/6 12/22/6 1/23/6 36/24/6 +f 38/25/7 5/26/7 12/22/7 39/21/7 +f 13/27/4 17/13/4 11/16/4 8/28/4 +f 37/29/8 3/30/8 10/6/8 40/5/8 +f 10/31/9 3/32/9 60/33/9 63/34/9 +f 12/35/10 5/36/10 61/37/10 64/38/10 +f 6/39/4 9/40/4 17/13/4 13/27/4 +f 36/41/11 1/42/11 14/18/11 41/17/11 +f 9/40/4 2/43/4 16/14/4 17/13/4 +f 35/44/12 7/45/12 15/10/12 42/9/12 +f 7/2/13 10/31/13 63/34/13 62/3/13 +f 34/46/14 20/47/14 26/48/14 33/49/14 +f 27/50/15 19/51/15 25/52/15 32/53/15 +f 28/54/16 21/55/16 24/56/16 31/57/16 +f 29/58/17 22/59/17 23/60/17 30/61/17 +f 30/61/18 23/60/18 19/62/18 27/63/18 +f 32/53/19 25/52/19 21/64/19 28/65/19 +f 33/49/20 26/48/20 22/66/20 29/67/20 +f 31/57/21 24/56/21 20/68/21 34/69/21 +f 47/70/22 31/57/22 34/69/22 50/71/22 +f 49/72/23 33/49/23 29/67/23 45/73/23 +f 48/74/24 32/53/24 28/65/24 44/75/24 +f 46/76/25 30/61/25 27/63/25 43/77/25 +f 45/78/26 29/58/26 30/61/26 46/76/26 +f 44/79/27 28/54/27 31/57/27 47/70/27 +f 43/80/28 27/50/28 32/53/28 48/74/28 +f 50/81/29 34/46/29 33/49/29 49/72/29 +f 20/47/30 35/44/30 42/9/30 26/48/30 +f 19/51/31 36/41/31 41/17/31 25/52/31 +f 21/55/32 37/29/32 40/5/32 24/56/32 +f 22/59/33 38/25/33 39/21/33 23/60/33 +f 23/60/34 39/21/34 36/24/34 19/62/34 +f 25/52/35 41/17/35 37/20/35 21/64/35 +f 26/48/36 42/9/36 38/12/36 22/66/36 +f 24/56/37 40/5/37 35/8/37 20/68/37 +f 58/82/38 50/83/38 49/84/38 57/85/38 +f 51/86/39 43/87/39 48/88/39 56/89/39 +f 52/90/40 44/91/40 47/92/40 55/93/40 +f 53/94/41 45/95/41 46/96/41 54/97/41 +f 54/97/42 46/96/42 43/87/42 51/86/42 +f 56/89/43 48/88/43 44/91/43 52/90/43 +f 57/85/44 49/84/44 45/95/44 53/94/44 +f 55/93/45 47/92/45 50/83/45 58/82/45 +f 11/16/46 55/93/46 58/82/46 8/28/46 +f 13/27/47 57/85/47 53/94/47 6/39/47 +f 16/14/48 56/89/48 52/90/48 4/15/48 +f 9/40/49 54/97/49 51/86/49 2/43/49 +f 6/39/50 53/94/50 54/97/50 9/40/50 +f 4/15/51 52/90/51 55/93/51 11/16/51 +f 2/43/52 51/86/52 56/89/52 16/14/52 +f 8/28/53 58/82/53 57/85/53 13/27/53 +f 65/98/54 59/99/54 18/100/54 73/101/54 +f 62/3/55 63/34/55 71/102/55 70/103/55 +f 61/37/56 66/4/56 74/104/56 69/105/56 +f 63/34/57 60/33/57 68/106/57 71/102/57 +f 5/36/58 15/1/58 66/4/58 61/37/58 +f 3/32/59 14/107/59 65/98/59 60/33/59 +f 14/107/60 1/108/60 59/99/60 65/98/60 +f 1/108/61 12/35/61 64/38/61 59/99/61 +f 70/109/62 71/110/62 79/111/62 78/112/62 +f 69/113/63 74/114/63 82/115/63 77/116/63 +f 71/110/64 68/117/64 76/118/64 79/111/64 +f 74/114/65 70/119/65 78/120/65 82/115/65 +f 64/38/66 61/37/66 69/105/66 72/121/66 +f 59/99/67 64/38/67 72/121/67 18/100/67 +f 60/33/68 65/98/68 73/101/68 68/106/68 +f 66/4/69 62/3/69 70/103/69 74/104/69 +f 82/122/70 78/123/70 86/124/70 90/125/70 +f 76/126/71 81/127/71 89/128/71 84/129/71 +f 67/130/72 80/131/72 88/132/72 75/133/72 +f 80/131/73 77/134/73 85/135/73 88/132/73 +f 73/136/74 18/137/74 67/138/74 81/139/74 +f 72/140/75 69/141/75 77/142/75 80/143/75 +f 18/144/76 72/140/76 80/143/76 67/145/76 +f 68/146/77 73/136/77 81/139/77 76/147/77 +f 75/133/78 88/132/78 96/148/78 83/149/78 +f 88/132/79 85/135/79 93/150/79 96/148/79 +f 89/128/80 75/133/80 83/149/80 97/151/80 +f 86/124/81 87/152/81 95/153/81 94/154/81 +f 79/155/82 76/126/82 84/129/82 87/152/82 +f 77/134/83 82/122/83 90/125/83 85/135/83 +f 78/123/84 79/155/84 87/152/84 86/124/84 +f 81/127/85 67/130/85 75/133/85 89/128/85 +f 97/156/86 83/157/86 91/158/86 105/159/86 +f 94/160/87 95/161/87 103/162/87 102/163/87 +f 93/164/88 98/165/88 106/166/88 101/167/88 +f 95/161/89 92/168/89 100/169/89 103/162/89 +f 84/129/90 89/128/90 97/151/90 92/170/90 +f 90/125/91 86/124/91 94/154/91 98/171/91 +f 87/152/92 84/129/92 92/170/92 95/153/92 +f 85/135/93 90/125/93 98/171/93 93/150/93 +f 101/167/94 106/166/94 114/172/94 109/173/94 +f 103/162/95 100/169/95 108/174/95 111/175/95 +f 106/166/96 102/176/96 110/177/96 114/172/96 +f 100/178/97 105/159/97 113/179/97 108/180/97 +f 96/181/98 93/182/98 101/183/98 104/184/98 +f 83/185/99 96/181/99 104/184/99 91/186/99 +f 92/187/100 97/156/100 105/159/100 100/178/100 +f 98/165/101 94/188/101 102/176/101 106/166/101 +f 108/189/102 113/190/102 121/191/102 116/192/102 +f 99/193/103 112/194/103 120/195/103 107/196/103 +f 112/194/104 109/197/104 117/198/104 120/195/104 +f 113/190/105 99/193/105 107/196/105 121/191/105 +f 102/163/106 103/162/106 111/175/106 110/199/106 +f 105/159/107 91/158/107 99/200/107 113/179/107 +f 104/184/108 101/183/108 109/201/108 112/202/108 +f 91/186/109 104/184/109 112/202/109 99/203/109 +f 120/195/110 117/198/110 124/204/110 127/205/110 +f 121/191/111 107/196/111 115/206/111 128/207/111 +f 118/208/112 119/209/112 126/210/112 125/211/112 +f 117/198/113 122/212/113 129/213/113 124/204/113 +f 114/214/114 110/215/114 118/208/114 122/212/114 +f 111/216/115 108/189/115 116/192/115 119/209/115 +f 109/197/116 114/214/116 122/212/116 117/198/116 +f 110/215/117 111/216/117 119/209/117 118/208/117 +f 130/217/118 129/213/118 125/211/118 126/210/118 +f 128/207/118 130/217/118 126/210/118 123/218/118 +f 115/206/118 127/205/118 130/217/118 128/207/118 +f 127/205/118 124/204/118 129/213/118 130/217/118 +f 107/196/119 120/195/119 127/205/119 115/206/119 +f 116/192/120 121/191/120 128/207/120 123/218/120 +f 122/212/121 118/208/121 125/211/121 129/213/121 +f 119/209/122 116/192/122 123/218/122 126/210/122 diff --git a/client/src/main/resources/model/simple_player.png b/client/src/main/resources/model/simple_player.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b7d62d9519f3410b12562d94cfdf65cb02765e GIT binary patch literal 65324 zcmeFZXIN8P*EPHoLhqo0pcD~Nq$vo9K%yXBI)c&#q)1iiCBXvHMFngW1*Az;YJfy~ zQ96RO&^v@4AZ5QfdhYi<&wYM<-|y!KWD|1j%(d2BbB;OYSUbv4Uz3%Ij|l((R&6bH zV*r3rFJS-!9rXoy^891!3!}Hztp@CC71bF+N7XX9+ZS||B0g%B{PJetp%1 z=(*O+=PAMx&Jd2G$zp(9bV1CW$|YT8Y#^!{_^0dE<8UJ@@5ghf*Xs{PgVVd$C!Fzh zdme?|^@VsZ^3U6;AJ2Z`J1zu37!UxI0|mhTbLny++%u+Mqwha||Kne08SQ|{oBw#@ z@5>Vagax4YPSig2fA8{tKSTo%{@=a-^X??TUqN%aJ@mR+s~2?t^58a@4B*J6_ixvdJcEK{H1ninUH#WwU!;2Q8<@#|Zw3@1hU)V!o6VxG z|2v%jU7i1Rw*TEb{~HSazXoaqrrA~GzvHKXUPk}Y=dbvOO4hDd3wHMT|Z{0U<^*=w>#UQ~wKloDG5_9JRxGHmN(LBK^ z5>?Py8O$*2@SUxL1y-RB&|s6X_bvWMh_@ra@*A$ChZ+q?dzx6M5BWyx8||)kPu=<) z%xCW;8SZ+4=ODE!gd*>NPz_V{i}zC1w7JU^euEPW{2Mk5GlzQ;+(zd+VRa&RjnX4) zZydt~rl!2ClCFG@=b{VOcMdB$J@94NfbMZE^4Mm7AMf+#u=o}CP#dE(Px}kgFJ@{L zfB4w46iY2^n>N0NTT!zj8)4+-{ePM)7mqO8u}MuA6OFVS2o9@|xRCx7^R#NnQYdFP z@NN9d%JZL&G9xap_`cNUqD=T-e3RvnWYpHGcgLn4lAT|ff^v4a5dVSP!Tm#pVSaPN z&;K~fe4dSJN&==zzSZ@@`#C>q#_d^O(Ov#0km(74#+>@uneADmC%Mpx={V##oUjnx z{E45aw;;$5%m={BDOjE{Hx{_1`7s0=rT^`LJ?}d_Dy*O z`)9t+I0?)DC@)tVcH)@Bnc=?ZE1l#{uXq-U^2AEHHV2tikKN z(1v~tOUV`?<9hJUy-o|3@HB$Yv!B0Yv5!XNBR04geBhF`9$c&K4t68DoyQi0v30@K zagD2HJuwWf=q-}#=)-HRXr(L%bG5$O#s@X|_xej8o??BIx3KNAHDJ%OCQP+aW$4AL ze`2AGqpva;l-61M>NESMwBwcC4Pfw%+JnbPjJv5>&G z3F$ed5O~~QD5y6jp-t@{C@4b(mY+Rxth~-9KPSD>`Hn5LZb~h{5BrLy9 z@vaa?lqq)|g@%1J4$E8RAWrCPvsJe3B_G=qCbi15WF08#oVa&8R&!z^vn!KMCUo<3Gs+us<(AoA23}yyZ$o1&w|b2Bt}dia3icUeqmRYP3-|S za=5IQi_HHsB)BMtTfI%qIkqhz;r8;m!Kv{URJ}&b?gpLrx%%W_dAb1cDO^eidm`SK z3simeG?fx@^NrmLbRvgpiC_As5qxQV7UuDl4v11b-F4ck((Ys#4 z9)ZfGB?Pyauo(Zs%F1x?4*!n?mH5^NYF8!hS>kchw3y5f-X|}|3+6=8oIm^V^%(KT z`WgGQCzy&MJGEu}Bm$2Eu$iUQ=Rn82DY=<*64G-Oa%?AvZOa|mA z9+1u*V=Dd7pgVW@$Bi%NB;uXDXuLA^4;62>(Lt`eR^X}%Xj$Xmu|fn+AlVOIPYi7f zD0laqphma14+@{1`zPk-QxjrtYGyERBy!;eKAPh4Z#N=7j%TkQTcgPfT+xd;xQgWU zTvmG;h$+&D2}@}Fpk6P3f^C(8cLQ!G_f*n{tI(`Yc=qB32U=fV7J|74aJ{mxM#GCM zvHJIM++O#-ea^f74Gtz|DYl=Yzl`Lufdf+iEcf$5fXDgK~0 z;7IFsMdn2-&Axi2oP4hRstHvk#-6B$6%5SZ1jT%xi1fwZdKN6}XLaZ7%)Dao`~4F> zdi*DAja%{O^fq@7C$jh#)|ZuL_Za&&-|I6Y(>=pTAu6&j9~y_P=uitG>tdG8NB<~m z5RD9u7hY$2lv9(88B8CwJ(Z zdxefeYdze>zQH9H>fC+29wX#PVvnx{z@8W(uGy?uRTauc4Erme6pL|h?d>$`v+2@= z1TeVqpVJZcxOx)F)T@Nvj>IOnrT7hnT!dxyg%QJ2Y@Leo==!Vbc0coaOd$P4#-{07QXY(y_*XU zmmuBOlT6!wKJ+xVLTk{DKz3uMgeGAZ`odC1ESrMsba$=YkoVtP?I%QN_73AVz0ZF0 z5(%$7+2)X8d%|_JzM|Y2Vek>R-OHO1$0&b2T1rgt~k+P^6NQl;p3Zv7&lSY*v; zMQ*MAd8529doMB8nByK7RIz#>MRb?xt5*CW`PlFU9YbAGf`&-cf$1`wn|!RIK!$L5nM7X(N5U$-+%&5Qz^ z5%g>C^Ut&m3ne_!_R0u2TQuXy>wWmu^}WcQjL=5nH)i?cifo(;vR{X;MI%4Y=iH=L zumj8YIgbC2%>Nj1j1cc3_bSJgt@GpMCm7fV-@K=oNKLg5!7@I?twJGl z=@N^0I2!6Ab%v4{;2R>s`oV4OxX{&mYqm@tg)E$DI(?$3?C+A#7JSK~W##Xi%4zF$ z31V+M#%)J!TrSrF?njI1Xiu>GS!|1J{6^DI{PW!ziFnA?os`dde);Se*_~v9Mr-qe1cjzJu|QA zWU`)%f1DyB9)Hbh+mT_TS|mQre)tNtWDmx`)c=vi0O$|l=gp}O?jPQNay_H&`wVkr z{Y9vkU^eC0FX0w5iqw?b6rPJ1uCtnHgIQVDtsNc5O@l6reW3|;4cU~|+X6wcp>)lg zi7YhoP>cP7S2zb=h9mPcHGC*tiih{JZQ>Z$KBr)7!4ZnS+Oj~8>>;~T;qlS)5@ik_ zxdQyx%#%M}`JTOevom(h&Xbcz^9J!#3*$+_ zn4pwBVy#j_OQ-G+_F7M4tJt|jyUZsu%FEZsf})Fu;?PVcYT^B5BdE*6!}bpG-HBcA zrk#G;;9Uh|=p4C-Z|(rY?EYO$e^`@TmGGr*r0v>_7Oa?Sv39H6oW|l2mX6V)$w@dx z{mauYPOeM_Cc!ioJ2`!!-I`Ho=-s$Y?N6H&6)0%S0R}DGgY{w2c=@5_=c*BF?Sw5gz)Frq^qpocH-k6MRy}2np^R~}k zdkwJ?MAmW)zC21JQQNp?4N0yPekkhsWpZ}_dihDk^-|hO-K|T4bU@Uq%VPH(%WF7$ zPi~>4+D;C;QjbhjB_T+fs5sK))3(t2KJ%Cv*NMA$82QOm!)6(z@ft4ip`MDDT6j0l z=7ZPMZ>BTyy7Qb`8zUH`1>@-6VkD#-V@r%qO-mQ;IhNRCmzdZiH!QqGBKD3LS~bf~ zA9poKqi2eixh`4JNqAoAFswp#JG67x@xSS%axv6?FSf3U>v!r!e%46TSWz8;Sgx{>n(` zIOFP|T>^x_+ta^Wv56fT(tsrFu`T{$5XwYcv^w35D$zN+^^;O>D+67n=p1UN2`p=- zvCpo1DRwdZUe8@I@7#k&hqiA99kc|TeQ&x4%7$ARzheQiH_YDzyxK_VH%(V9FG*(s zoU}1dFyrfzY}N_5i0}`5$q(vC=|L+BV>nJR?6WT zs37ZvGtFn1k*&X=@J~iH_&(BXUvV?_XD-)Px>S($FpmwHADljh&(z0QZ_UNy%$LA~ z(3N*H0VKH;tXRmDc^;>l8M~sv;E}P%SKXQnTTr=Db`A))O00>wV96I`zCdEWhPoNk zQztc)>u>B2pE&4bR$ZQ>4Yjj1XXHCFZEI&!&Js66g@FxRsP8qxhmKxi9bMik2g)@a;U!oC3| zl4U1&{!CC02lN>jVUSCe^(kAX$2RGs)c+Cv5%J`h`$cc8v z$qAORs*K?4-Uc+DS~BH~&tswOvO*&)&ky~Fo(LG11Nn6B+v`-TZq%IM|6`Tm1FBVT zZ8KP}vx{IO5EVNzw@8-z!OxP3jf3zX=+p4!?NPvx33T~xFuvVf2E$m+Rq~)LSMhnv>gL3?@q+a0H$UPb#xOir8clA(N*X)+k5;Gg{+r`gJF4 zHdx((@RY@x@IwYJB0eje^-7^1VQ4%3GyM)=kuI$Ho34j2$h`ojF^MT_-x6CGHtZf2 zPLzsMRPZmt41({kRSFA5fwSxQqFt+euM|ps6KKeKc=`MIUbPSAL(SF|0iFGV0kFv= zi3D~dr_NjcNN76RoD#Zb8J)w!Bfcrf(=9dTn^6F1V@BAg!7A^)zbVYx4Ri;?)JHJ8&n4 ztyGU>Zn^7!5}C4Wxw=5D_sAkcE0vQe7bY!4K}{}FtN4;Zr>5({e(wHZ@d3Bw$Lq$P zA{$A_O)rf+@Qd*d{0ioo8{o;0jX-TLN@DBi+7OdPFejF06;LZlLp}ayJ6_nXYg+gLIdZj0^*?w@E`>UA)kE>G)$B}Qo?PJ6vS1uZd#B(b1|LLn zYDW$%43)70Mjg_L$ko7Va(Wwwx3Y5dk|hV_U5#Gce(r*|D;_XAZ&du^y15yB&B(UBIM$!M*5_<&v3Rd+s$b4`(sC&F#PxjjbXwR z>k+}Y{UnngXno9ej5XQ7z;r~~$NytV|I7>r2$J7XEM@{Yrhahl*LabLT;y)kX|GMB zO|4^Z@gC6SbR^(RDO?sdEQ?)(on38+00Mz3^Kc(iZtAQ}ac0oLJeW%+6Xcu3i|Q2dM`pL}`N^?S@{#zHglY1MfiT?Et(AZay~0ChR*hj{^k*!Yuw?}gpJ zQX+V1#F^y+boC`=z1xEZMag9IH6ulKB0U!kG=P*nG(X%xa_1!)ES(qKGcbt$!u>;c zKlpHQr)po;cK>i(EJF5Y8WtBg%B~R%6ftdlWPwQMeFa4WX-|uw7CeqT4@VS%2IW#C z%!b8~JtpsN>aP+7gr|`AX?ChfE4F@o5Uc+eKL-AaAIT}%Hxsv19&u*eOmV!I*q#ll zUNs~6J8>=&&_;OV`eFL=A=(~}CR^^OCpCbZw=Mz!hl{IK`&;m>T3{ts9hl9a3XlRPUA+~;}G z>SlF+Q3^7?{)ENgf*H*#YkuG%{E!l|uwHdmc_n+PswX)Bdiun9 zkeU&WALakM5xM+VN(b+m4)NZB<*p>%lD4Vd!D+S^fyXFy#xB(mL`p*qAHr>4+IN$u z2sCQ7Eux5%k^0kMyGRm6mxOHRw?W%v4HGjMLcZgXPMaop()IH5Qv#y%8fW-Z>Gq%u z>;N-p^I`O2K21~Td1=%`aNZb-XKb^Sy4IlLL%OU7_fb#`abYA}gWQvLDP2juw zXUP=vM_n2UT>Cu)Jh%0Y1i8d(LdsD52R!q=}yg&hUatf=@{;6lC;;$B$FIp=AN_ge;$8dlzZElGoEUU#) z_!W1Ui(-D^bwL{J&z}2se@btxAEOYZveQ3Z(_o^&D7L)P#SS*ie;D_E_z|4l=>F~G zkGF+>G4zT}O`srcc?6+UR z)z5snU*p03+R4P>ZpGKN{-TE=vA}uz{5g$0*rlXrhvQQhZr}D-yVpB-`xhbT7ONfa z8jy7e+7gm%O*`c{^Y%Cbn$cj|bPAHJFkS!Erz4_USwkd@t!!3s6v)U^0p!YgP-i&r zqEXhAI;p*%(Rs>rwW)Z5;v11=JPk$d|-dU z8&l^1lTQI)R3k$Pc+WeX*KuklbbcLd=a>xckoq~O0W6pS5|qehaKp?tD80W_5+U&m`w{ ztlrIrE_`8Y*UbMc`Q^2FLeQL{xp}j(c)?pinPgvYLHN9Nes(o3a5H<_+BU$mozqY7 z38x+5`;dx*JgkX$o$z(#R&f20?zSf&qX6Z#2H1$r3Z!ajTf)~>_V8fNfCF?pgOCA$ z@gq~->GW8RS#zI7nLp6++@pD%%as&w01(4ZN^@1))FBRuuGI=5S zJTMwW65aM6oEd(SEB9_!=!gqUyPt#7!DuEcxd7j0_ve+>PhwC!e=F(m+`|iwK9?(Q z^W|Pr&7w=`(70JSRwR-#n8)`u`nt!Ad@)_G=*`OgAGkyRX0$SRdT)jvTENxT(zqPl zvK`i4ML`-6QVJj}bdMJk$X6(>pxdC@myf3r^0zMbFNPLJj)gVVhF+pe zOF!AR&9vcnS^0KF{EZ{|PjR73t_^GX5lmEHFnTwkbqgrjH*d$}q|79?bWZoB0V3U_ zLPFp0ONcsP-vHk^W`i)$Kz&?EgKWTZZA4fAGrsSC_Y5i!e+^ z2#kp`2ou5TO#V8{a9Zt7(9o>Eem=37s0&$V+R&_@lU8GfmJ4r8;5UOm#vo$+(By>7 zCfjM-kKmT^N8A3GPAY*6*<;&~w?9%MoB@{#nz6Z1A%;T328gob#C8V5cTd(YoZFOU2&ebzRI8`w55BI(9(a)!arYG7OJ+$iy9kcB<7d_bLx~5y z$7m17jk+&V<|TP5H-5b!rR=8=m%nUxx@1Y_2&dSQ_wcnA%7p#Zc^bS6YZ`C`N=Eh1 z61vK)8+*>OpXNVqy#>C+?d^8Ti?Be0MJJ`8$W3#KSrwQdv1~o!H6_@e$O72>luM3m z*R^{+Lhqdvu5FuH04z3(|FYF<-S%prqHLR&H7PG0fMI4o{XcotCt zo8jbaTV`p1ACNfQ4p={k`FbJ^lI@R0WP_PL=9&2w>PH`G?*v>}(wjc+9Sq1bov7iY zw!;Oy-1S+16Gv;p&t(TMUoV-CQryRE>x^VMTHfygsKsoWBN z?fC?&M*Bs|_~*p==FG8S?gbe<@12%xECr87`M&fG; zFTl=eQbXn_P!+oeF)l}4-GV*8U^hUel;Z%EV5lBbsC~?mme)YQnHQWHXSBz39V|#J#BFBfwNcEj=qlnU@-}gB<`hJFo*MT?q?7} z12<(s9OkV48m&+-xVlAc>Mb8I$2Crq7LC&&H0A6vrm3K|8#BVh{N&<|nv1Z9F;O~{ z8M4yem5Pv=<;ST77K3N2uSDrwl*P}QzK8wEBs~JqTl+c2m(V-TiTD?xQHNm|Ubf&1 z0)nj3FK6t1LLQpF7Z5y}(5)Z6pl=W@=A6^KogkN%CQMNXxThHGZz3FG7EpY@16?Uy zSry#KZ?ItPp&ad=7bemFF-ynZCZ|KBz3Mbgxbv#yorD)WbBu;5-!y=TAkJsOT1Gnf z{3_pQBw6IEq;YY9y8BJx4ST_jiRN~1i}z_PF<)k8UZ-_1FPmQ~? zS?VGmtJD<6SnW!wFfWDHbOespunGVkb!A!1zsFQ@O~^Sv)Z1A?xhm_gu2;06$*!{8 zB71bb`di(@wP``R@NiwC?hL9SQq3cCjiOL9$rN8eu{dkV+Z|!pb^A7-;;8uDqP1Y( zU6qs$ZkyCsbGn2pruD^RC^LDwv+r)V+$#0c{b>|m-_5$;jr@jIr-Q+*9#6 zrvsv=E0He*uCq;+RVqcbYBru7PdEhgnKtyOj!ox%p^P5YB0_*ox?P|y44k|5E3ox+&g<+;zCw?Ok_ZNr`y_~+s$~cp}&)1Q-@d{U^l6%N1!ab#p ztqjGRQB8?W6vGJAhxy1Y2Hv(^&;v=vj63eIEnD2$#;f!)PlJ2=ulR#AmC>&`3Xp}d z9l!_T_>OJO!pEx!eu$ig!%h2hi){?vQX<&iwVa9G+xaxB3_FrJZ=%4h`oPVc`jzCu zTeRVKIrb+B!Nt=#U_1Q0zI1%U8mMREyRenZyjg5xGA4y>)>461#bCo(A%}Ff9b{Xa zLpdyC8PX1L;LXc(rYj&P{GQR!q10-Zv;Lr%^&3LWVM8K2E^m&p65E-vr*QVi_EoA+ z2)G`!t%KsFip#e)&g)V6uE&|vTHAhRBsB5>Q|o_X*ghP!?RiTpcrbbEeoT&b>Fe|W z)s&xB#+qq(1%gt$@5g)OPamii3b`9FmZw>_A8{4qVn5vJJ8;C++~ zk^0euiB+p$S0OLqRS`_5s|#VOBto6nCO3TBAconDwyH{5DlN5*d2;KcB3v-v1C_Z! z?YKY?0>Dfe5ixWiXrN*Lnm{rQg|-l^q)m~PDphMJH8*x@f7UXcqmTA?aO4+${uINW z+>A6RBC}OmVswet%jwoc>prhiF9OA6@Yad^R}GzFYB9(k=Sj%JZDf#$&y3Ki=gmGh ze^p++RW$20D)KX1VtV>+!3n+KPhfMZmv#L159HUvFNN)kul!>tApHh%9EwZ&(J5)| z3>jRdN-P0XhF@GF8r)7lh(ssAoS$Hm8AE0xL+v(2z@hHE8>xx0X;KIN4k(^xSprjr z0Ib`~KQ*H5E=Dad>rD;3F)V6GT#B3`sc@D@96h`sGJN#Sh5zCXSI+ zkf%105+c}ln8VKHwAG)jUPqV06kxa`P9H*?W^T3gD#S+YSpJ%f`1LC+X*Up;7cc4z zupUM|I3taEAMY|XVhwW>i3N_L=;hF7DbmQjV79Znr+0f8NScD1!p-cYne^o0lnuCf z5)eh{FH=E1FypLx&VveuRGC^r9Fq18S{~n#5Xq~dG3>q)^g!HQP9X23;14$=HsHeU zgIRx{u(ag5lu;}n4T8Vul| z#aN>Kwlg8AIeKVh{mH&yWSRF18U3_m;C8~>} zDui59TC3S zZwnSg0>N5wgYsC@xb5XHMZy#LZ)FQuq6ImoG zgdZl5Ufl$0^}pnxB?Y9kRVa zT}K`RDLw3md8L?eD_VouKKxILg>~wKoD0&ZWtTD4GGK7bp6F4czlVBi9>W+8| zKd0|_WdFqwa!Xib{Qii^C-+XV>bZ?B85_Y91#|a`ywIM{@FwX$4ou7$wODE0grkd% zk0&F=@{etNtYJ_t(B=tW`SgxlOr_s!=f3q#aqUlcA7)MSgWpC?jsn$Sg2onLxPYsDBur&yQKG!B;7kEt)e^Sz4r@n>ms(l#!yDLvtLWf2jlDnEp-Q zO0%Q7i3166<=2<=OXIGbdpZB!)~V;trL5%R)$h+2s-uBD*y ziH^metqh1v)L;Q&BM{gB42;}%==S5|-C>63R#t4tMv`ZS-e(=Wo&R=!e?-PM)MM}tV%H*OrAWe=GfZ@2iuub~ zamh+2nL6t|!267U5$F*2VjWh5c+Wid|n58>jtB0S|^ClD`?s;b|?B6W0Gtz?Aqv2aS;mTh3En=GYIqq{z_n4clnsrP+l(61V?c zjs{6!^oQ*uD))V3?hyHj3v9h0J%!f*))+HhfC)qz!Lm)o2iQ?Y#}8?;)`16mbsgn? z%naemxI-IaxBt5zg=laUq3e9#G38LUNucy8(h*X12WD7_sjvZB zd8yGE%~-n>V`EE7@(L=j*_NuV3}IiBN-QRPz&-9_aM|E5b!G=H37mCCaS%igmN?OF|DdkXQkQ?|&a>{O5A}LV#UUP&QqaUd8GVf_ zH~EmL7g|k3ry()Y=HN`&ueNlmCW?}`d?0x5P~9YksF~y3yv;?p)FNl*dXVcPx#iyvf#|z?8gK?AD zO_*Tzs#p7h1;SU+!~~TZH&mmXSMj5O)@A4EeA=ZstNP5{c*&HMRu}LCl>$MsKKpah z%RLccM(wy0l=AMuD>sl2vyM>)$8m4Q_N|dP(SwAwd1-%&SWbfJGF8OUUHuBzf|5NB z`D;|2WKfTYT%@lj9CPUYAncul=rGr5E|v)x&DKD*)#_h>e#Vway%VCW+!uYO$<0`M zfDFC2IUTcL&k(TrtDl1`9Vq@7+rLe%x~EUY0^N>Bs$vCTEzQkyPHuXJkKns!y$2bn zc^OwK&#|xa?mF@(C9pcaHWzBxT5#?CZ~S2=0!xlUrW~S+z|G0CVe8xIaIn*5eir z;v}Ta$*c|%aJiv;dFjjc2rop!dlOa_#v?>3mKBCU38EXHq(js;lf4eustM>EK^|yr zmeD%py0jH#TmU=yF-M``nmdc(E`Sjmmq!I3S&Yf6NkD~HlZ0=`qpq+(M!ruYZ}UOkx0^j4vWN9ZXrTBel&JkdnPm)tm0*);4&1bHvx zeZ}Sva8lKtj-QTpKEgM;|4|! zT6G3{TIVYlEEb4Tvkv0~eXF^eGCg4Ri?Z;%{bW2vlgfsBp^CQXL!$^hW{0>yaRnfl zK{;tMWf+d>d1r+0F!ji=d|z+_7^+f! zfFtF(;O`ES3r+a-O*|dt8mV&*cZy>A2!3ftj0UB1D0yfbK&lE@kE#V68A>ip22$}n zZ%N{$o&dDyjQ8nE(4R=K`AMgq_2DV(1=c^lKPo2uUS6#m%$p)NXRKm|A2Qn7?dx~$ z6=*3>C2)A>4njD<;HqDWb5}X{Gt)OCqrE~$*^`PurzD_G$XbP(p?LTnItlHTwZ;cA zZIscQ(^QpQL1eQgyLRtV1`RXTAZ`wAYfwjOLm*-MlkY_+^@wh+Wm?l;kkmy1g0%ZG zN(FS9_lf@PnAX$BOEO}Plv>J@?Aoy?P${qYNQpd+A2btA{Ko5d}Z ziQT6QBdqZQm(mc92D=a5DWEE~(}1^8EQRmB7}F_O-L}SFJw`+Nc1qce)5PqBwIeS*; zK=%ADkKxY1$qW_9$9zP#J;LjK91uthHMjt*5}CZielN%{LwK6?{%N<*_x`FM!hGBz zaMN~76Y1sIU*_##X8*L4LD~^%tsTtjZu0$Z__Fivr3yGIR`bafsO) zyRyDM$gU4Jxe~8=NSLf&vV!*MI9U!dRWuwW>4iE7b`rDM)J9bxXYe7xR-Zbub7}mh zf$*Q)`|pRstf;4fz|zwsvn|0iz>TA11^SeO?39N*8}>AE;T5is?|iW)(fHOdVW45z zR0Eawsfcb;9G1_)v2yXdk|{0qm~S!NpYyja65-V=$YS2yfr$pEVZq$fRP8$@N@p*f zDi_)2+L5-{frC{6JZMrU%_MCSY4pnE_g#wF5RS<9wK$HBg~R0{WWmf!4m#`GD~sB7 z-b#xi6^>bu^{lPpoz!2N71F8k#{t45XZM#E=tQtA46+=_e5qztlL8`@!LuBxBSE3h@BFMp7RwRdNH-q}liEa51yszQB!e`-lhnNi zu~fW1J*7{%$dH$TKv8mj`Kn(qNK z{?wm8ae1)dv07>!Yi}Nl8&^8hF2?WsaWjMC@@jU~``xV-FD$wlo)=#Yb+KT;x*sFPL%?rFq1X z*LwxwUWXH80f=+mzkXLUcC1j(eQByjV%?wawqF6cs1b=nEHl^SL#HdYEBm|*#MO>px!O>25_#~ z;(A*O-)T0(x`=~k)KlhEqeEH>;Q0Zg>|}NK*Xi(-G2-~5vwMCrMMb~w(Y)wZ-D(jy z5QDNNi@9kHuH^LFd``ijKB*$YQa8Mk_29sb(DQDym zy?xkzO*Rl>b%|~u>V`=iP!I(&Eg7@s_&$HWx}(CK2Et&<7;;zuFAEq2?L`px z`wi%T7{7$&Wdl)SSicX8PenX}Rt~OuLy1OfGkJT|$knPYY0o0Qs=ZM{3O6fs=?IGdO!^ zK994CH}_(hpCde zRgGyG#I+2CzrAUl8AvTrp-aUmsxB8~T#chcR|?0`%&E}1$qZW4qb12==K$#8?m$kAH46XYn%GT|sT{9vc}eSN$JDk}eMKy5e2?xW&CR==X^6OETpRy(1u z%OqhxMK=zILiA>(-{(B*LoQsB6VTp3%X||6dTA1qi7IjV@x%B|ZUj|!NYOL{>a-ep z4eJdFsf6Je$fU;hT#fm&KPujgT^MOQ8-^ z%J_~w-dDL*nBSl;{@emO;Q0x+ZNzMJsIVumCpTkdA$rOiM$K&OLX#maE^^fJb`Ce^ z^7+8WXKKy}Q&sg)1Vz4+^@*Z+5f)qS`S}_nflKCQ0-!=TlA+w`_#!RfsKcuXvj;b< zEC@|xR1M1YkjwkRN{HSZqgr3+vfx0lP0KvR%y zM9iHqV4g}X6u^>Qy4`xDWIvgU_<`A>LD28rYkHx{`;}Q&-+liAx?W*E%+Iy8DiSEH zvA*>Fuesiascv)ICh34lL*7?_B2Qj3V1Nyq$S>>{40OaO+V=I!?ZPG>MPFM7^Jd|n z-lp>d@`BxF@m0YzSSEa<*{Lr1-_GnE0K0#8Sg_m0V@S}ldgNs1mVltcrJe`NQotZH zwsdTJwTwUypQR}ZKQ$A=$_Sy?6N)0|&tljzwSQGsb70cqJ)VyF>3^|2@iG+QAu-D^ zE=<}+OM7)uABQrm60T4?Kl7^Ckc>R4Y(YX#*)OQX(>2ag@I`o4x({@+)zpN;?>$3vwqWRkC&&P2V?>T=rPCdmY**~?0qrsO%jOD2Bka9)6Xe$Oa-%WVdP5JOh%qF%gB_QxcJ&($^7amk+Cv+v^X?potm@U~2z(TiB0GZ73VbByXj~ri(FZ6eUz0Q`c16~Fr#$rg3Mgi9J@|ZA0kE&~e`vbuxTfB>eKrORq)}2@lFD40> zc=d_qp>E@|_s!^4mS?4i^Nv83_Wdv#aLtC&Fh;>0%*$^-764V+oJJ5h0ip_@LPZ`iG%bU}ZL*$NNJV)4vSd^~BD^*12tPko5oX4xR(tV~ody}lq>eoNhrrc=270*Pxe6TVP)aO zs+3RZar|D#w34{hV|?pk|FD3E1F_6A*Bh)*LH6CGnZq^%W`G9gdMWHp_)#7ZIGtui z)G;4=-rs^`SH3(Dt2RgN!g*#HCslsR5n!Yq{QB`<#UAun+J_MXVM=Ko@3;NE3H+AM zeVat!WQN3URMUIAubr^k%Fs$ptFJcnx(|v*uflNSL6AL_EjlUaD8&yIPb;`7^hXvX+k^ z?Zpu(mE+u*EKESr%_#aALso(RO8Hx`Mr|EStMG{N(hS@%Z7ZFlsW;PTwNd z2{8&+bKc{JT;s^0)l2B0TRhf}FMD%q)wgEA(LZ%o`_O;Et`yZrND)1pgp??|Pjcd=Smioc1d+c4DeDD)e*3T6 z+Qb#B^d4vR>YDiidbZNAzV9JeyxHj@$QVr^MkQkR%1?Kmb^cpsc9p`kdb_(zONS3! z;8a{FbwfXg5l!nV0dj~GLia6I0nWcb4Yy>GI{=oKeI>z`Kgzm{g#83rI~6`@ z&P_W*N_xYF1^PsFB*V8UOMrRn2CKPH(P;{!XbCo{|b25Lcs!%N_bu`$QQV* zDDpMvK;|x<`>WJAwIme5 zoR=w0;pRZlN;8Wev-F5^Mm{XXjbzVFSLK>T)f}e=e((zWN@~T`M?{IQ%`2v*WI!bx zBExqi|B+9)sMvZ3K5vKB=uY1_G@mGz0^|$!l%y}|r*}>lH{1$*pP&%xRz7m~eIEy$ zZC7>kV>V)+9ZcG{y0ON07LxIn{tV4|Nm~0nhB0ZpRST3bN{?Ixa$>L4#>${bEp5}A0RY>%J?|+Fk<*W0%QgNh^a$?%^N$`J;5M zUp+lI%6__O+NV00UoE@_w@8py@1F9n{;90_LqIoi(3~%*+uy-Jhb18RJM`~qZlwiz z``(+74B}~5{F%94Fxx9@c8ia-x?)7AY4O$Qe^2I42|Ww@1iCKMi4W~;1uTGc*`fi` zAM0d61TYgmvER{gu|~J4j~!aUmo9j@Pgjzc)_sijvJJK$=3dmy&#hOum}q25VsD#s zxM6A4LYu{UXS6D`RT6XUblNu#-hWMfLgn5%>&0RhSF1}XQKz+vdoy+@vwg814V z8{VtWS0P>K&-+9xUc z+Ab;4y))7L`O}xuAhsN9 zk@w*IJvDnKX36MrB?}%xK zC5QMVtSNyrl$jhQPSAj_wS4O*2V2C`$i1S6h@}TMs%fAe!-lV{^E4o$`THHMl$CC!8ztFRP8lVjN+=oYf2<)aJACI;Z|ceN6Dt* z+Z2n*Zr#Vj>$1tOM-BRYQa+S}!#<+S(9d3~(hP%QA1oDYT34hWyNl19_x5L7_DsBoQ)!8&?vs$_&ec*+@|z*2 z@q6W=J-wS86rc2U0xc@2?%RHTebELX12I<)e*gS7B~6TXNCjB;ZGM;rsW|htan#Mg z%3lP!z(gR=+RATU`?n+|kD+I^e<-t8w~pB?0H5!J4BzpZ&r~je+ujV&g{F1C&DE@$ z$I$EFd0{~@6_0mk16VHex;q1guH_akig)S-b|x1`*praI{) z4=fWrrAiB}Fc%*vWrcKL^D z9hh1;EJ~I@x9!KStaQoNc|TBq5^t~!);4_kH*SJA?pV-@L+Op}RH17Kwgk?|IG#|+dB*~VH>d`VZ0xQeVy zRw~Ge!%X&yjXRh#kH>yJr`ZiQ>2CLLc*|M+)?uRCeEH;kIC}d-`?-^aa*d0Lbhfm} z&ishEG0k&+i1f>KK4AU`q?Iqs)cQ5hZrDhrA(bxH=8Hq5T@`*2J+df-mhi+UJ5m{I zwGkP6!2fGjh(eI`r5Ps@b+}ajcaLxbw*w(JR&DL`Bu`BX^nZ3~l2MJxhN^O5dblto z8|s+iYy(P7+HC2ET^0}GLUMP# zx`7hE6;%#%qcDt(JpJG=v42iiA_@E}uk9z#uKu}n#RA^nF7I-GmR*J(tyY*#PN~oy zsL|#zL4qtfUor*Dj=sAzyXc+O+Afl)(T*N938?%YoEglPk@>W0^(0tS2CoQL2}5?4 zo#HA*5l>O=h1vJG7$G+u*)kl-@_KeSdwiJyI<6l)WjnT1?rJU%iYr_E1X@4X{Rg2! zp^mNn2mZfV0sRxpB5z#Agv^}oH>ID}aJ`I;@p(w!)Q0YFjy@u2h;)t?US5*I3u`59 z=4d!qnHc=BPA_;zRgVjK+2H!5`LzU0>JH{reTQFWP`ivF;clH8WxpKCf;42rE3`#T zC!jZMY7$(IOH~dFNst8Q=4dS6`}JLy$G>x#(Eu}h#};-oPWCvnl}F#ZRaP<65#DEM zMqWM7vRbpc?EK>m8+6mj=4Ggr(0QucUTkGlxFFVf3MgUd-6jt1w9$)u3te^+9^pDIw&wh+ z!GifkyGz{9*3?ebmMc-+bw2vY2WeKid-AiPdas{624x-`5nEvn2(K!BR&{GYN~;GF*|*sZ4iZ5-z|*gR6%z3W!Gs3kUZ4H`l>$J zAVS%ZJbrrBxPry{KmR`l%RIC^v!;K=t&ncErV)jRKa`VK{Dv!1s|C`27y+Pr)tpo0 z6l8|*j8AV%jxYBc)h2SB6yM{2F?btYC|@ZvwTd&DN&-)ul@jc0Co=B=(Z9oq!bn29#D;M)t;AxUodrZ0G5Mww+Koy` z8SZnz%W{SpKn|B-p$Uh4zZom;>|Z1Bqe1*&dhG~&e`nK1iZIkp=;;-DlU}+wR z&e@YYv^gz5Q>5*di-tfW)H2TXf2)P+R5}wPxUp$n+*qli!dmlSt;NX#-D2_K9y4)- zl^kqe!GE&$?fIvRz;=gVi^73fKRU;zCM3QLiek?gbQo?Dmug~!-L@?Z#=Zx!P zZV`X-_vMbpqE+$rA#KuNS3|tzpz(3r=%bVK?C_7*r9uU8l*x_OUlm_t#kLx#os-DQ zEty0qX|LqJ`kNt%>P7_z-e`Sz_Tg}D!^r=$z~vTWtO|bn3cmgWjH*aDuDI?eF| zJ@cfdkK3;hQcsrhdmV*A2{#ea%@rp1#BQ2)w)St!AFL5O*Bt`8E>5d7He>FcSFJlG zPZ0IBjY#HG0m>-XJ?QFfzqJcr+&QYlJ?z_}wk1?|? z1CwX<-r-SnD_suO4ImRd@o!L63fKA?@cyW-mdU1|;$y+ZOKJq=k>BPFq_ZyCzVnM% za#)2_3s53i)-CH!cl`K|{rqgoRjR=Yni5f~Ps;3pBvU7T&Xl2B;HumYRvCt8gxr}k zUAo|3cc{AT>UOgQX_H{;TT)wFe# z#Nl<1g|mOwpcG)9$xzAHH~VFtKzm5|lboH-pU4kHNZ7sIf{2WK+`vt42`N_KY77YH zVs{t@ENG!U1YA$tsu`s7@uoi3*TCD`LTS$ONf9~OmK+uErB~5j5LGdv8)2vi@lBPf zx5g)GRxz<(nCXZ9v=X|&O6-AF6qCib#CyDoFAXS0_C$rb zt%*^)0KDHaMB?s7+v=lMNch>}Mt^4*nDveELzI4=>B~R6iwbWb|8S*AS^TlpFHV?= zGA5?FH80cnk#@Ady#}AK%BqurWah?BxQiD^EOKcKlnL=!^Nl)#;}pzLiK$|N#8za_ z#>2mRc&@Oi-V+OENcE+7?PiO@Z(6LDyvTg~h1`l5i@a{m~2 z(!Oclk;W*l_2$dS(a(rs2KPOq4@Rg$QW1nzGOY2AP)n@p%ZAUYxk*gJqn^KFdV9hLam}IH3xP8~r`dF(tByz&}6Z5gD@Ez@&B^I6I%upBZ49>_i z%e7yBj$d52&yJon1LZBtLOMtmUh$16Cw_~x14j|+(NE5(a!u3 zCMeusO31rVld`ahlFKs z8Iskkgr@sxgefur=jV;GrJtwerDMNlA`ac)JQ0tR5g_a?7{4W^vmmG%N2;|=LeabL4*1_bS!FT1Jvx2KV8o;s)h2Er`Wbm35u2+H>9h4iJ5PlxFG z5`>k4Rz%J%g5=2g9ny0scOY%)Yq%#@_xrk(#IQ;&7Nlg3I@h@&*OPKe4z(WMd}8qC zGOs94JJo0}HvQCY(hz2sTU;>n8}@tb&fe#qAf;H!i^dB zOeuq#-hb;!ys&!I<);LB=M7%VO+H|vnXP}^UQ&2fcyq#I4gg(^IV`rom)gWEea4|0 zOH-U$zj#>0KO3|*{WR(p0nAau%4c?l3LO?GeR?L?g3iTV8O4jay-z!fcdLeskA#T= z&MbT6w|HiktGSU=mu_<^yUNtN2Xp=8q&8$Sd75a6AJs69=3qK}oTyF_egKzWBJi?- z{yBqv{L!zY*bv)9aDS}2V3()M_TF8&1>N)5n(7m(jD07*`pV3L-Ob?~5v*@{-6=)g zN%a^68px>^^2+=8kh|hFPPw-$Z2>*vuoXV0>5b2NX&E(s-$oa8%0J5@8}$7^!OC7b zx4A?U+Azmtjs|CEoGwMZbJSSy<`$5iVJfH;tjag^9$rj=zlf>H-59pelz-#G*5OJ` zA>?^NSY*v}-t6BdGEs3;4)rmU#F4?y99Ce!;G@QVWh6DVWd7D0>4!Yr^_6vcBecgu zZ&PjyG%}~@WX^^_N7J!YU-(IcclI>yJK1-&zIJK$YQkGH42!2SzE;_XN75rq&`xw< zzeaVc(X!0G5r0n-uU~sxb~O}HO7g@OLt8JP+o0Y1t{%of@dBkuWFkN8x)fQUBQU z7$v*I69xjE>4(s>G&jdfmL0>iyV7@@NAY|2*Jl~2l_WHR0(=ftMll5u5R8%6)ufP?wtmZjcG*Ao4)V>k*d7sHOs*TQwXuyAc6n>jdw_`aS(lypx{-Q^ z%cmgp>cY{@$08?ice?2=DWdEn#a{CT+k~nTa@g@5cEz5elF{E^&NruzH*=w+3iZ<# zOZu1BYpF5!D-ZZ3{nEg*zQ(%QU{7<43~hOT&$%(`;R2<jWFwKd50?c&U4*`1%8 z)v1gZK5&2lLu5^HNaJ%MPs$Krxd)mxN4^_))c*Y&|)tF4{Pidt^r> zaXvhJ$0Xj`D)Y#Ps84NAxm*(U_Y*7#7n0wz`sxgSmX`-`<9U%zAI=`ha?IT9-SJr; z+`e0~DV31oLJJ$H7gI7ABVLJmn`}Cc`l1z?9+!8{mP6AMf70r$M}k zudze@yby4aIRYt*APhKau+Qi{240Gk+|bLLb<1hgE$n(5|Bj0KphY3ad53!JL5=K| z81&4-+{_nyWoqgiALUTX+db1mNd4g2eU*33X;BnIT(((=O`{dn9 zJ8G3roL)Tfg*$%&UZDgDW(MY0xwY3GFrv>RYZ5kxbbTHIefNghMh`<%%hSv!u2fu= z{8uox5`;chYa3DuiCMF!+oow^7<6!@WpPqnYLHun&-f};D8ZqR$G%S95FB2anf`Sx z@>bQ=vwFXf2Zi`!o5QZmXTOrzG6YW%ccm@7F^ZuNZc`K5%Y3LsuaC5Zc>efMJZ+498Zk$iRC@zg zFUU9q3TK18x;YD&)P4wwsMQZk$B-8G1j?^sjm~7o>d&Y6kDXl2XsJt|@{I*QfUN|K zR(ekLdi>Ei{i!* zjb+@EObakC@n4x3Ik>T+a764?Pyu+Zp-Hh_?}>)7C+Q>pwGS0CEF3sNMhc=9KE(^$ z)+lO0wHnZ77d0NfKL~W)vYqwbCjM5rosLc}?opa^-yDEK>8fgIi@r8XYrHgU{Uk4ADMiqH)?hEhj^tVEcu9Zx({Bs+(sq)s-?G_XnAz+J)Wueyb z`u>Ig@YZ+{7=ujDj^B5uI*BUaensB=iH)&?b#WC6>wdCW@*`=nGIT@sA~>veM85?@e;8v+SWqf}<|&{~+h1L4c*n~L zPC;RQQ5(PSuat+s7U92Ersr<;k|^`cj65=@z36&eLBH*C>R8ER*rTfUnJ3}hpA)v_ zY?#-E4`>LfS&ZQK&pz^Ch1u4FKVOIdQ=X7%V+L=m8>E>JUKS732M%pCs;wx0vN!D% z+{Mxl@@%Ekap3F>eT^B2rx-FSyqi$}47ftos(zr>fRM+NgF@h3!PXY>&b@^yHtbkd zd#?s(AsONLC*f?pQIV^+x>Cg|{K~hBrW2hR&-&G;njK@i*5>!)3{uwdx8~EQ@WBPf zbP1P{5fPYIi|&*2B7ne`#eiS8JP9k{e z*Qb$D*F~JMg9|YO%*Zp5K9>XvfX$Ny4<9fl;)be|`3-(L)V;84DI3uDDeu2?{lJ0# zEnqiQis?ueb)gSUez{>&g`OXlyB8(Mdb1xs_j}<>d`*Cj{1uH8&g5VemsLbs2iyUb!lx*zE{&{2z;4!W_>*GxKqAr2cL=cjR(AR9)#XXsgRF3TvkP$lG)AKQ zkFPf>e-iF)yrxi6%iwkMZ^5pRrcYA~jB5@hG~oQTvtP^klec^-u8K>i53T*|%eUKf zdne@tsyuyl3Vz^sJMOLO8l&It>y4VlvckI815e|2lybz=+&&TP*JW*cUoeRP@k<_O zv!jCT{v4p3eRUk7hHR==(#zsk&iVYh+m~WM8pf~ycJ{I9(XG}n_p#@hihtXhAPeRb zhWyT%&t6$N>^q;)I>QN;YVlho^|OuO1t?r;WILyQu!tODP1$$ntj>rWV%O)AC)Kb$ zM-J308DEXT3vSmlqf2GjVI~@=3WyKV8arQBMHyI4cjL&`Um&#D6Uhj>ao*M68v0{` zAd(1D&-KqfnZSW2A5MXp2i-h^x<8!N%$(diUf&7F{JcQ_tT8M-BY9n?`RQjtyKgzF z?z$oD>Ex0^Wz3=#InZA}+nXO(oE|H>a<&z=0&YAAKYDi0q(=uI?aRMBOk!bYI(txM zj=TG(OU8-|Q2+%64tIVC+yzUoU^sc3ru{|xpa9Xz-c%~p$8@ijYPrnB!SaRvv2WZk z*V^r_5y2}?B%}$)lSh_kl*T zy`Y8P`ut^R0=Yhf|CT$b#^^%5#rM=jz`;xhJv_(4*vrTfwq@mJ_iFvTY3W#IAR2nb+`9HOmoNID z%RFMJ64ZkmkWD(Qy&{}+`TtjdNK-MSsWLD~zw;CQ!15N*n7WsR+=sT?;5k77JR;0} zHPqFHs-0G@Hpt%=-Z#Wts}}mvRF!M^;)vacADEqH+2Bo8)rXx^!|5eLI~Oo^wn9JlQvtFd7%30`sOs- z5q|nlIupTjLx7n`Na%qfYL}?0nZU|)^u;2`tWM@AQ6kwbIgQYlJQGve1O<6Q6KVCt zo9DlPyvuKAw)eg9P2%j)kbFLbMeRo8n@5ZQ74A&R24se9MxhEBs&AtdpbS!X*+H!T zpp=qUlbkxVn_i3c5(cu|&wS;8q#{!He7o+Q!N!J9Lta<^E5jp_;S$}O-b%;Yp<}#S z{&c2*K|mqrEFmN7)8kO%iD_MaG60UBo)$?Vfl?X21-78yS{;VJ??*^9qu=0N0C$Dj zxCugr{Gr(#Jl!Seim}M-J%cXR0=8+x-Zr2Ada_IqvhRf+HvF{D?rq)U~EX~~lrJv3+qi2lBjHwQ|us4Ib;HIS=syAh9Jx^^wA#zuYo zj83pt86&@WVcvJ203|+HZHQbyT{V3T+JxRcs1&ry9F#syuD(5<$+}?4Hq_s2j`zR) z_B}p|=gdeKNMaHm()-UMO=zl2h^rFy&1mw2uOxBnbHbtJoFaxZH)@0)(KUzTT#I?= zt*WS+ni3k{UjO)LYpK$M2&i1qEyT%HOUsbJ*h@IT_<#X261KLJrSy>%4BLYB+O{E> zw?IJ;KAaG3j_=|FwyEcAo>$MPgM~HFKPQsJ2UvC4k8jR{3dxD994sytL^CrF`2!6| zgO(pZAuD0T^!j%;W#`DFg-PFv0#(_wjkDbNe_rOZ8b%F3H$A()i!y&@^{SOyN zZni}+Lw>6P%updSa*QD$vGOG#`g-f%?+apZ@c5nbvke!dzJK4R5AjV5yX=0OLpD&M zkM1)!G;q_laTBoiyOP2Nsl6M%lHcngm`_hfzvLjd%%^EmKm~)bn~(tb7@4^*c2&r` zt%%5~66p`AQUFG=$fsPGv+$KYV2sS=9XK819F97N%-R~wIdStYOb@jeCwTgV$b{bI z!TXYclEw8`A5^yJ6K!fRPj61iszic006Cbln9<2NfV@2{q>o)Yx0_OSM*I{kON zQ)}WuoGQ8y+CITLjmy`-{An}?3N%pB(A8B?WmoNUJ&1qi)Oj*8TkG-3a=Nzl^imNh zE^erA(o^ZtG4ENY>#RU$(nod2%DfK~uwBnJ^4Ro?+Un_(%|aJTO#;OB2&FH6ScuaU zB+zwpp{VCVMt`&Q%Z~-5O>LOrmCA8$cM));V7g`AA$~zj9>*2i8}v8hou}-2(ol|b7G8xhUNomIYM;luGi;~_u{+P zwn6%`7t*W=$RISGxW}ZReS{O1+);x_cEU0L@>m@h-E@c`DMgcLjgC=h)n*C?GQG5& z(0&%X3v(Y27Z+m6v%$^k-EJoUjEwkYM>Y_x+Cug}b}Lydpmi3?dnPKHh4)4KSows85MZK*V3Ne z&M%sD*!IvyY1!1E1HF18yEzQZ(ShT}`5Wk7QJ>DdI5QT+xr*}DnPXZRIC6U-Vqin z1#G|S0I!eGhCjqY9BH^SA)#{cA@!jd>fzMyK5HRYb*kz){rSH36EajEBJsWF7G!@1 z7aWol0Fd_xmM5%q09}-9)a1%jR^Vq*%o8|6{xlnB%+tbd*iwrkMLfKHxujUDI!vtfS+ATqp57})>$?iw2RMD*va;+|2- zp5`miPbhR-CB^!!fqDyp;~MLCO44`OqZWw3s?Ddv@U>#ue@%&;QFh@2PXH?G^- z69Hzp(Q2(tF#5dC1gXn*MKARDj&=92jqLfEm=$!f7!fe!LoncR)dlE{6aaUiDwWp> z>^ZPd89!w5eVxam^7_HwfrJ~E(#XDUHvL{(`S;7u_x6I?Nm6flNTZ6Po!%50zBZR*h6gqE ziyQ%mXDU)l0e48iZlj5&Rem3ap>JYiy5glFx=C|z`SFcU;JqkN2vv*t7i`l=AgNwez3#zf;U-%2935c3(4SJoa`J-4HN1YFXz!kwi;^9;_$^XQ$tndDI{zxC-j_O&~Bk2FwPGMKtZfe{L_P6hc)- zuqt z2C}5>eP;l8$^$UdjY5CT#Fc$++dvd*7V$wTGwnQVqsGVf2sT#)OjBUQC5hVq0Z|(4 zy)1iuTTvp}im$1{qV_k6Pne^?(t&nM*Tdo;sFj;FXH!_(#m30EeUAR!^!=OQo)5tF z9*K-qidroa0ZFH|OS5YKJuKB283b**ro4Px6RF~24t^Pb|CDW2_K!;N8V(xDOS za!A|Q@d&ddAQPs+J6s{m*am7AP$ZxxAAB(xzFfcHN>v*Ar|oJ4(VNEqs0W_F{w9v> zdTdayE0`dmmL4_KrDCCO&}$OdKDOGgt!4>L_?+&8;Z29s?6*T#P@32*X3WR0uYe}1 z40#Qy)8&Hbi;OB!0)*=tlI`_e%HP$b2*<=l1AfNl^%Js#o0P84>$x^+3n6BC9EktI z7;dYr`4_+PRg`HSY_z|t`TE{ZG8KVq?HT-XQN~?akY2lM(WjNxGsvjz{gB=WjQX3i z2+`w$q`11efnOD6{c2=D*&#Zou*v#wU<;00xO@|Uu^nbAeI0+leFy&o{GAm{0X$WuYWM4()4*?eOUo_CSPvp_rV}+!J3zGWWA%+m6hTsBzJ+rIdbh< z!7}hd`Y>l>G(ruEDKASq*Lk3d0_HA6hk45TB2yeydcnr+5=K)kT8Zs^3x$N~C5- zSbc?3KNMIIe@A`UmIW7Ec=$vmZf`1Ku&}@Cox&TSNSHvcPT|ew?U4l&uY%I0a#agf z`p7QILX46k-SBq}e0zdAZ!bQt2G7F9KO9)apCu@eu+3i~P)R{&@+mj%E zRP2ZhUTD}W1|x6&M%<~r-pj`>Oa4!xs_edF(so4bI6?Y^9s5Vs`PXGA=p_%1(Tgkk11|K z$k2{YY>u!??8G7mH!#(I-=`zQJzfXAmOf!ogXntV^t2GJ{gLIvH>*LPGi!*7Yrl_( zL18t#dTI77-0FSZ$GsJX)ES0K<@9&?hNtJ0n1Ti(6FAuE>_g$_zP<-Ms3vtFM)B6L zZ^Gldsj?9E({%47Sw#pG_6}wPLrk%PRPe=5_{^f}9>X^l zrzpXFrQxr`tPycYU0LE5|IbdLRj@gcQ>D|n>%k+RmBkI9h~3%f9)m034ajIC=ak+9 z2j_WskGEc8al|}A#AB1%=p|{a@IhzhGz_k#Jjbby`jQ2B)^qvfL3?>6Y$i4+PQC$9=S?SN6#%rO2Yxv`6W1mUq4l%2H&iEcTGTS<+Sk%Ulji>n| zK?ZJ|@n;hm9i=v)vdeZCuK@pRmkQ8>AYKHXZ32F2JFYWzjop&8B=sJ_<@zj6b!$ya zk{glX4PDiSlGR+zI8UXsetJ#qVej{_Pd?#Jp++)~67}D5It!0bjzk~b`+`NeP&{IF zc_393678w}DYnQWFpWOc6N)J)AHL_&x0*HabfTAl7T6;+*j3spa+e3UH~OB{j4)xO zQbh}W!)1d1En@7RqAN*NX=!qWkIz6cJ{9>)zE6V#1T!G?;3kCVsy9Q`qhC*tG-6=3CoVb}n#BIihhMz@oz9Fo)yBcLjZl|2`TKf0(1H)yY z;6&Tu_y-ID!^gpP*%2TD9g{Zi?0szZwqV=4S9$tA2`$uiiD_(Rf{EuE>|1FqBOnH8)qnnpKdYc;E=FY9`=ASrN# z-1-J_3=Ch|IqhRBolF3kJHsxXYN3{UrXfhtJ$LUd5e-+`ui#c`;sIE>3IoQGfe0>r|K*cT+ z80tZ^Xz$SdI7ZI-Dgyh_W52rNmuh`cw_k8Y8RASjTaRLGKbI&Zae#tWI_oLfXUJDN z`>Utgmbv+4m!J;&45Q1%9%R_`oU7*M>V~iyJELs6`uwVbqV_DvA5-(71Sl%P;0vyXLwa7w-Ri{uei3*Zp~7 zM-|Er#rw1JO}wOxx&0UDXrP+K-M*gEIv}rMj^TY0F=VM0(rgBZQet!hs_PSw6@x1) zI_K&5b|ak^mY-no-+JuOol>IN4+X9d)|yHKc+^o@!=6~W@ffc#q&*81r!XOr)B40t z6+({X>y0gSO>0eCmc#YmuGii>k`F6L4QN;WfJ=3JLIB!PnF%{hy~n3fdBueZyk9f@ zOTYRLx1a(c`zojw7Knu_eHKKDlOz_|FxS=al=V~DCqyo+hvu1d^?cZSK%0W^v$Ma+ zbN~&3{~{j!VO8lE=B7c~`i zq%%m3Md6NOp{BHhLW)ULLgM=bOomg4pAk)ZVc0u1Il{P%_nq_F6%QcqwD4+^_rQt#QA zh7BUY_=pA--v^eu0IB^TU?VwsEe4x|*56wH1cbi2lER(mdgnop!K}49N(#oLUGgZx zl~n8CfNhq=aT94~!r(<_sE+y?r3pyXM-0V)PS5sg zq$Xr#Pd`{ca`iqm-j|@g67o92H1{ue-~I!QEFWZJ`NHD2GLb4n({KFbh~ z)Bs6qbG*h5NtqTq(GZ*YmgS=cE%%mzMOsmvO#hbD7ZnwQr9t2E(?KKn)Y_9lxf9l} zwUn&65oe$pF(11c_4ZON>F@`S4|!cOUGV@z?$@7x;SLJP0y^cO&EFxFl?Ub!8w1QG z@ueM(i;WAi;0nY6kt&#p)`0W(=v_gGFs#Jzn6A z4=syvSHEIoY6)7#@wa*{vgKswkQY7J!;#t(Pw1R-4Swly0FLt}zUvm{zWg1JV|1QZ z{X6oIdUtW1u)lE~7&wyD`5`G@xOE$Tpk%JCGG1v|bg*;a78DfB8BWM~?%>UB-ca|s z$f73R)vWe?k!AIJ*Qu)a7S$nw^(QND505O5RU_>Dlu?*XSPhfN4t}#uSCvJo85vIv z&82fCfQnz0A8y{@^zFx6w5Dxe?rx$=912lc1?+$CV)5z2!E zp*2^UT6$_Kd=Lbj#rT_%mi$P#2mQ8M3tfr>bVo-9(l1R8AY~U1ijU2y{y6Tk1&bR@ zlI`s@wiz>@!#d~A=F0HaH`&p>_~I8>2g0#li*LEBXA`)4x@q)H*xBFWc>LvL1a7!``_~emfc}lU`Z>R9Hm5s@ML4QVeL3qE){eG1vYjGo zu;w5@l*Wqx)&BAsurPt{4wA!oDx%~W=UUa$sNA{m{g#?+XRpxW#OV~;H~IuZ{Yh|l zgX1d_mG!+{%N_^kNWor_9De=w%Ei!BF=r0%K=H@c1}#&I-^NC)eV_Cl%$=!8K+dS+ z=eOj}Xi&s!Iu^J&{f1+u7>cp>EST*Wk~s|DCkf3sW;}QgKG?xck=`tzfU$Hj>d=6d zl*)_Xn}G1LKO}K*-I0|%_8!EnHi*}}_I-M>MGqHHl$|WS{|qI63&THJoLh{`Li^4r z3QcHQ-GpYrn#J?SUQ*s3xR1(fnvY$(Y-zY{etW)#Hx1(tb7UL#JmHJ*c2@n?!{3Fc zw;#K8G#;{h>d-JG+;d)>dl8dZl-I%)lL!cj}O&Brm8{dDV?MM;*Yd zqj-@9#eBB9a+CtE@)4|GO>kN(Y&boU$hcO3$f$)LFkKoc1oYoPkZBh|b7d|}T1*Yk z(nz#S=6EWU7SHe`lqWGhXs;7x*72m`6XdbLbXVnBEweI`m4vDk*XE87q)g9g^M8mb zEk6qCLVsj%udP}^O7}7kRaoPXLK1;ICqM1KN0oL=V{JUg-|4wqVAl-!EHbzdgXU`!4}AI`?YKv2S*n=xWyZVpr>e z&E=9NgyI9RoQ-PPf*dTNL`mly8zkqtotC*Fpk-M^!~JwaaGk<9OH&-LUNE$qhU)U< zxQ$00HMTfpA~gyXkyqc^>b(@C}K3 zEeqHzpd)m>cGKPVPUK_Acvc7|IzM2S=fL%(znaCk?n~q7G^+uWm9GvZ|8s&LSF3>+ zKhA&Zb3+p8=nf)LE%Hz+S#y)eyemXio@)j*OuocbbO%r~b?G8CG4>V0_9TKOIl`c2 zHV;`0gVOsq|3}`NheOr>594RX2q{~ZQXv%CTajf(3t2*0vrbebvX-nfQ}-Q3QL>aQ zrA3yIeVvh{Qi$w3N%nmkX3q1T?$2G0`~F_n=Xd>{=bz{K?_6`{ywB_X+Sm7aXoMoc zJ$J&oPx`@hVNRRVmWO0|QVzZhY&Mxt+Z1!3l2~iUJt=$?=AIRZX4z&nXxnICpYTN( z9!ijYHAkN5(caVBUAk?y+iy~`vzCoiz)qQ&^WRDlh#fi?&!u#Uy(Gec+N~BdL@RN~hU#uZSE=@Di%;9uezmM~Rxvb2@kuH%aiwcsubTWEX1JcB{A! z^u@swiCDb4uk9c3gZ>`}31{{V)eeTOERq4#VcGPFce!-V85f$6+J|hIEIAHwc`u*t zZjrhMr^or165P|_FYg~FQS*x>qT8(lawQn?lsl^o{o#F6^8RP(VRw>pu0*-d2B{G~ z)U?ub3S#-M2u#qWogcX2`i6mqis*wd8C(!le=r%tI{#>tfIdqHH0v_kiQ9+ z&i2U=H;GkWS)_T*E~m+O*S((Feo`Bw&ky{K1?N+8H_4A1eEnhUvYGNC!Rphvt=@FB z1TEwuE#X@)-N2?qa@SJ(Cx;cqnWF?hAICQ}HJ6kR_aU~VbEJqIBg%8Qn5PEZ>! zqBRIFzf{fYj!^xCD`0$5LJEi-9ZE?(Nx=;II(31s49EO%IF58pwO} z2$#d><9p!&eGa8kSB-K=7hYN9gO2Kl+d2-Q9u+=@U^T=`rEEIoYPff zq~^Z*S}9EZe@EV|sy31Jyf~BQA9=0j7(1l^%+AE)O-H~v$U!vYJ zf8v<|dASGw8dwTnSQTXWLS{t`rt`BN)KUj~>F!Mg3EL4cS7K|0zG`SJa2%l_n;*e9gB|@C;+-RDoViKgYt;g+~!2IogKI+@VcPf zy*&{yN;#aX$|Ew)Klzz39x@nra+z)0Cax7>G3AD9$B(}|`%IoNysY1mp&D`8+SNMV zQ_Y=4nl1EXVi5-4Gw61$91dGaB}8}m4~}b~du)nggXq}z`&O_4Wu_fnt^xh$+CFBl zOpH(PH!oM6B}~Nx^M?evXE}F-@i+6ZzBuN3RgY)CV4+*2p%h5n0WyE z4Vea(#kx)j0Ao!Ry#~+aOnFGVxC~^|l^4UvgWR)P2U4?XjWcgu&R>PZp!2~KbquTW z8uy8r%M*R|zO88=yRQF*J10?l1Vz5Hn4h-UEyoso6BWXSo{?pePZzS{a68a_P=S2s zCYrpw6?^S2rGuSyrD~ZwSra8AFZ(o?FjFeQZ}aZVz;WW@v0ZfR-Rh5f*m&7yR(eO+ z79|#!me0mdPK>E=PCtN)hZ?-cpLx&Z!V5x!J@8V9YOk%(JUQ#ZmH8_n3CAa+Nw7aN z{3VwMvZoTIZvalV-qf;Oc;9B_VnNP>7NntY#8Lxh+jQQOn5m$U$s@8e5JPP;hjP&~EMOC2s?9@qX{RY+*P^ zLg=XkTX=q2;ET9|h)7J$?mE~9>TXIhq9b zN(Ns!TVl6e5xS_JrITt>i%NwHX*|7j(bw)unT)n#@)o zH(~g-iK1J=CD>JCBq#gfL9^Otv6{8{#2&(4nlL#`6nqq6LZ2rh;+^EvAXoo|n=I1r zp!;`Qd#^N3~I3ug`v*5L@OWa%jf4n?-$cs8k%IIJz&?s$;jv zjR85~Pn>Q$!`7S2erGj5fu%0cKUx)CnDqxrPNKUTgq&46`l@pB-?dTlvIE8Utn^*s z_uXn!Jtjm`nw*NeJ8V^YO#_P#W(HCheOzE_9vwXEsvfam=@7m|6^5IJoQ&1)SoW zKym_VM^j1;`|;+xi)I=iMHm#ONAy|1 zO8tb1sfb6=)qC-(w!qVOeR~>C_b-L3RT~KLli&xl{Dlx-j`We;E~E8MY(%-a=Xs38 z;5GDCVVw)hb8JG1&yJbdCZ-+*a0UCIKB_Rk^;A}*cBFQ#|xHyqzn;(!%Ir zQF#`*Y#Ri)Rygw?ynQ(BZLku5vkBn=d=8-(%4JgKKIAfeIE&_`rM0Gw=}42K3GpZuRcP~j&QB8WTrRj!JvY8 zX08MuX^~%y!HpE@{mBNBh$<*;V()xo^kj>u=D@qE)c@le8n9vqSBi)042)P$dy@vir-^Kn zPaS#Hl90ch2O5mb76vl+-4GAk=v*Z4cvfoZ*|NwTYh@j=HCBEAPhPaO5)2>21cTaC zFsM(D_m|$UZ7W?eC<}O7@1mLb0F8!Q=XLM8ebOdQH$C061z$#UjOXlW8;~BN&W#L7 zlr3j)q$i3~3P(Dmo|`7AG(mT$GDEdr-?P9cl2uY`-BEO+-nyea zi6!S16Vj#cp_g?1gf_a3L`WF49SHj4VFwSurCEH|6dCaZPkb!IcBH z9Bw;lE#3hYb+>fT8=hs^1>^kBPrsaBF*v%ia$C8O08=G%kH{f&zqV)M|CAkBy6*2z0P&%hp#lqy1!2-^oSwM`8$VX$t>m(CM%Q| z!I;5v*glxEzSO^+tTZEBPVj#vw%GldVB0_8%r|KUtA##8Z>HU;z^e;=dq0OBiqZrgS+*Gh4yVTVn zoF+RteU#+1ALq>LAZ-`b*Qu%_5}$8xOt})ezC~XV4(ax>Q#R#syhiLHvz00oaDbew zVmDppTGD2hoCpcc_Kr|)Ez8Qe`s)j??^7Jn%Ja2h)1iL3TA?Ca2KzU;uACJbw_OLj zcGm2p51xU+F-i1zRYH7xhU>R|@Sc0GRA+>%Zj&<_TVeO6q||d;P<4&*jdH+2)YHJj zXX1G$vHN!S_#~%ZyppPn?sL|eI+UPulZ~l4u+Oi?C@p|hk74ND``CeV$Q(WmoU?mh zR7HC{%69fR1qK}G<_5v?K1RCaIbkZ!mhjlV#Ydg_4oj=grPw`MurN%ZU*7YjJ63rz ztOr}O(ybCRIdP1G4qSTKc9wlKl@UT&?PCmOsblmQ5jZC-Xm>55z(ke14M~cdYq?6` z<>s=jaEScWR_s#gplnkhW|t_ak+Y7Nv!}}`-qO7-`k>&Egiz@t2_~6c%%4Co?B1_a zZ4DoyYg>-^ZfL|&>X^%|+6ZCkkp^d!e@a3jdW`n%=#wSC+a_Aa1aQ8B;4S3QK< z4U^!~?Hf;3pJ)3}rS7&%dq zNpf(P^L%<>*xTSI1m|^&QbKYl6osyp{vwhu|;Svyur*@|Wg79Fmu3SMzAt zlwhL9k)77Y)+jw>p^xc!Uz<_lp%wru5(YjE4^ckzZQlR3 zjGljC+W|Ogf=cR}4|ES9tAq)?U;)7q>q^4+Gw?{Pe1oabJc#PXFDZ`_wpBe^*lUGW zm`H%eK#Wg`!StOXHgbl&RH&TosCS$)gZV`<&*cC|9a=KyG`F_l{gdB&T}$_8hg1E6 z!~R=vWf;7R=f2YS*jmrTITi9zG(K%{yJQ+GUChteFH~g_en_746`F>xP?z#u2f3kz zkCrr$JqOkRrk*eRjy=Cz%LbJb(goz`^{m7Ya9%oKa*PxDqVW6XEb>Xqo&hk|8xnmN zvVUy+o)zdQ?r@7t?UkhsXMY{0gEoyCK+2zU^#|*&mD(0?1NaOXOv4*CdECG^qpfU)k^iij5n@Bh4+lb#b`SDq`k2o%#S0N0un6^-Q zk3NwxrpbfH<18=FY2YoHWBU(PTk~}{4IY1JJh)tOew!R&IH^0T>vQ%>_h$f<|8!c+ zkBQSAUGlX@xHr*U279{0;URxJap-yfs3arPrUIbZ)jeo$$4*&OYGf`E@7`e8dvjw6t8Nj%P4B(&fU(rE0H7va#ih{ zRt&Ntc0$jC#|#+|Ns!XRp63j`f;LTfXcKR4@}@#)Sl@dh>)o+B3;I)#awuSs@YY>O zy86OpWli+Ao2Z%N#M-L^n!Xc$uU>=&&=Q7rzu4$dEk^u<#I-kV8($zE(BaEmyG1 zNdrM7w^47d4Q69up5NRzgheoyO;(flbl&d48q6lbKseAq<%(kO3)E6W z_7!nPRwR-_FW#*N@APc^_TYdi5B~gYx_V|0xv|rG{NsWI3dCxW+g7R`L3l# zx%E>U{ey_9fUQ0E(P*wK$7`ojL<$bE2X1y7Oizg^@ZRiG_{5^wsAacn%4_><$(bHw z|G+(}P{y#Ci>-Q9&43tsA!WSr^;Mx9nWzPE^v8|;-4%KKt}C8;J?wCY;!|%B?RekX z2Ll}@FmM`<=_NQchr{-GBMT9f!cNB^Mt_UydS`#hXJgHSSpyBO+2WMh&+Yz4%L%c5 z6MdH_4A9%UwkH-G>+#UX9Oc39>7G^SGP_M9>5O$#F=i5KQ9-%udO@%`-&&upKfh1o)S|vg60!`7o z-AQ{rRYw~>A34AE)VlR2A zG3Y?rwUG+uTmf!{X8V!0l#1vi=S^D|ga>6%-a^)aZqzb~75U-3@b;~j(}vwG3Tbn} zMRE7{OE%RGVQp6iVPD^oPCrm$?wQJ-^;f$zlW(Fo?+^vI5+uc;^W7{?j6Co7dgo`e zqo=%Mo8LxhqxWZ-Jv5RiCwNkyupPLWs919?y7nAeq$~O+5zswGKmDXH z34vxB_Ug1M=W^{PdgTGwvVG|jd4yJVF5s?5l>F=L0ATw8Tiisa?oJOpqhm>!d7GZL zEeVIYdJnS4j2WK126J5g9`o?jy4*b#v_K(L_^pxG_WN-Y_7$D)p3ixGr5p^ z2`%-U>+H8K%##%B`tC8T6_)FFvUMf6j__DXKTtR%Wq_r&QnP}7)5WgaDd9}}fwN0@ zYrUjq2SCG03M;W(F6yU>jls+Z z?sz5ue~4Lc^yxkN9wcV;x$?#}xbVk!PsP`Vj%>?v>5x*m2W2_?D9dd!*Q@qBw-kzHDZmMlK=uN6CQka@h2W7TlNrbXoZaA*OZ_>q{+(8H10NeeR}yub$nT zuoPxVJ0`Hu>~jCL%f3Jq8*wUDiqWl#p<--GunDBX#{9bWo9xHYdXEbDSb!h-B=)?` z2Pwl)qLPigtQ=ph`xEsp`N4MPcdw_TNI)130te5;`7zNv+%et9C}yu@`WCMW=lh4~ zZqWCeT^DkWXw#TwVOA<^Y^vsJ=wj<^#@;is+-%D!oe^xu^doVS{rM3qt8vc=E3Y!`ez;3h&MB( z?lYhtMi;{}&vN1CWp76=umR|Gi+rlM7Zo!M9@->H;X^ko3c1f?Yw`t>&>o^9@zK*i zhy(t`l!1XtuLWCwv%)iUZ$lA5l&jEU9DFmlaN9$&IbUFbLrE~xyMwf_9n}{#{0plz z{+wwjPko{uv{R@o_^jsW$x9{~I*s#mLF0+k`N}I()SR(Y+HkGOmbr5g{sHla;-9I* z4Z`$Wv$K^JzVSiSsx9zs|91q>i>XQ@CY}UIi2ycY zYe6kF`5JtB0p}NH%PE)&ElF~RYpnydfA^b|Q|ckYiriOc?Yje7S~O6XxT%9gAFKq* zE?^WG?L%(Po0*T6*s%BceP>p(3S1POKjiti8!^7VhPvxDbEADuPjOe1Q>{jA>Dc=FPBP_*=@%ap9pW#WCX0uE!~Q z0F7LmB{Szqs`;~9naoZrodJ0xHYUZVSIY@U4m~;2n9Oo7#Qh@{dkce`d$b7+Ol+Hh zzd_b3>I>{*$ilhPCPZ}^rHm#m?Z$(1=X~60V;XMO84e~vzHC9&DWaU!>QIP4ENPm#X_xlP|LzYrMMbwZjvouo*Y<0koc!bD z30vo_ma`K^azB)H&R9riT(ZLI+{eDE$|uxH)H526yeHJ8F3kY@BTzxl_nv8mOZ#d# zuEFjX49d2#5kG8IK?9Lz{faTgTKV-4d}YSmGLlGGM9mGoy}3Pcm*GydZ^Fz`cu5pq zJWV~obPqa4v5-DnW5W-6SvsOM=#rl^SZ362l1e;FOHO_)dVjEc=B6;{GEqIAKcRly zTll*++|0$@Lv{v+;@q)45-1u^>C8u3sH>k$aTFy>*j>ol_KuO!X$L+~ z+457K&Io)GYOhVq=67D!WL=pIqsPvE9|JuspgLmdgP|l<8BC|aTX&ydmcI^b8XN7b zkp%9%ra~<6S%{QMOdk(u=%d)@q0v>XRNL7fUkKF6C%5Jk{=h}DPA)zAV|@1G$~W@L z%USmP@u!O2g%Mr8gEmFWk}U9^q3&w`PPpdI2m147K6~sv45dYz?>Hly@q#UFCB7jq zGf)s+mS}LqXQisnwO`Ss?<~gORHFaT&?)THJA>y2rE~h!!j&1Y^7D31p{G@0@y~K& zW+!?~2{-5lMFP9b%8DVq8q*Iy<^LKbyCC?>&%SU#h2e-clMu0%Z8?zsGMqB(sFVAllqfpO3W!DrQ}Z<)yvVxGOcB$Z5N_Xm34R5X7dTD4r~8W zs`}D;F8SWNsWO}Mx$0HzL19vLVXy7TG+NyC%LCS%M9**;EgUl>-(#Xr$5<+Jhu2fT zn_Aq#V_Ebx zvUzuh-qx*_LzN-7MX$m;6KMNOCnC-2Z(lTP7|=~U_GHHko_bS`;pT$}8zM?-J6Xa) zFq9V)9R{u-SC>D5{a6l102@`*183&9!*5uL^9zspfpZ61`irr5>)Tu%&o!F-#DImU z;kp5CVgg-~X=bluhU<1zAS{Nac`PR#$m*n=)#wt>%Mi-44HcBEnU)_Qk|m`xWcwEP zo-|CunwyO^+lIw;PKWy6PcxvU!o2w8kO_9-IxJ=I{y#Oj-UhQ?~kPr$>dOK*;G;6E8ja~_DiQY zrc<@V(ZgYzl??$Aqqp)ptjyeSUq|*e&Pe(c>91WrJnT_)SDvN7hvuY@`F559Lf7+U z`+KO7)k7Z#bd^zeNAAM{Vo;N!&GQ!{7l(!#a#byQeD>IhTv`yQ2I&rRvETl^PY%To z&aXcX<>XI%eZ&Arpk>8P$5PlOVBda*sDfz&8AAgOW0lI+PCj$nMnqAD27<=z%?YW0 z;BwP8pAOI1Qjk-0Pi|lzt@)LWwwXr{;XuL?*?+gZrL`;%i)dgJH{=}8jQW7W`P3S&~KCiEbDKG)mbU!}{m%*n* z3M@W#B_jfI^oC!mfyFK4C;MnV{C$?`t~b^(;so&J12*HE#26#|1^1UR-XfZO#$hom zct!ZFBXPwz^kDEgp-w(G0pj?4I%D)Jkq=!h?6n0aeMn~~BhCOr9^9Xzr&eq@=s_&= z*XT``#|ovLLHp#;=ua)z&(*U002Gwl-0y<}))-2P@yZC`6zJDy-C^MNxoFQN$mq-! zE&$xF%ta?1-Hh!fhcxvXg=r%rBbou*J+o$QZ{*HUzjR%15>B7H1(EXD0IkxizD9%R zxAR_Ix_jS-{^ykg&d(-7u6|W7J6aWejuXsTNpqomFkOxIkL8bNM1@hUB6BCp{AqKV z@pCjCIoMOH)32!AMOb{@JZE6@Qk2v?WoetzFClvQoSJgvhwL$^sh? znzKj1ecjy8hs(B~hsADOtFniW(Fe~>O*_8*Xlhdy8=XElSXQgCL2Sa7->+Hg(fw!7 zj05&$ba~GyuSC;RJ??)=qz^e2$2PrM49HowKX(dmqwW{+$*o=| zE(g$jhD`!m(fsMlw%v)VLmkYP4e&06<0=F5znOgZ;jRE=iuiWh?$Ca?0NjU{n z|F4@4&((gBJlFDE4c#~qlO50fN?l?u%9L!Ma&GR6d?O}gs{86(va7AZjdxT@Y+kx% z<5z507CmRo9&UNzYy2VisxVthvVlyhP~&{%*>JL|e1O4&x|AkwnzGD6_-clBbGz!; zyf&m~*)jD~aBv~aebuM^Phxk2R>K`(@{Q;`e(!l!lZoWG>D5yhJAq(^!M@ zwS$;NdyNg&87G8DnNJKqnB?#tdhn2+Y;;iFm|m*faOa@92@c(3EV}ilM553VY;Of} zdA*IzKce6a-n4F7>}pf<$=uk zPc(9EK8>IFTyh~kCsWW@ZTikZ&=`;755B9pM=+JQn@k;8ckQ-HsT5YpYHI6W0bIYpsqAFs*QMNABdWigo}PxoPND*9|O>ptD$l| z2Z>3Rt&Ha3U-vMAyMRvJJ=W=D{!GwfyBH+cwkHL-j~DC(`zS-2jr=@NzdT>TL8b5N@mc}-CD;=j0i{rZ4kd9hSR#MUgsDM)KM_ ziW{nMNdwu%)eo`B5Hip4#oJx}mCF5#ii`ymPLuf9h>t09sLeX|%&W1(nsbtJ4!Z95 z`B(Pl*P1w8W~+4y1^wdCx?*$Wre*oeebb-uCAj5UqZQ8JD}UO-zn7)VGRMp6!7(SW zPq?8I6FcU_5Po5Btwc!F+=plTz0p4^(wRX3hTCWj3SS z(pYvvckY_6UJ!+L5l}$)T;d)VULz{g}j|cls$f z1YgDEQb2QKmYjJ(VZwZvY%UU5)*SPm*2CFSWH?(`UMgqOCjpy%OB?zW+C^H8X~uwC zd&Y~LsmI#){YyScWn#;|ujuZI6gO8VM6MA$%tHxn~^%#}eq}IR%;F({M=D;khRV(p5{J(6rHP#D>q#V3d0mW@UMUhauv1>5LnK0>A3bMbU@EpAhm3jRk#H`fkFCS2YoU%qUT}Z< zpR3s~Mul`H8pZn<_J0R|hhmE1_4{Lftqd+aMcn$%L7sJVjT8IP`0!ijRzD1UI&eZf zw0*?zGeSO4=<9%a*S$2?{0${oD~{J(sbnKg4--z{==OeHsCL_8+-Gp(DjI;rf0W=b z^mAgI@rSzm91}4VE3a%##CCDk_;@s&(kX{Z2EG#l5mv09z9hr(^!w0{-pQYpzu#!~ zG6KREOG`0XvjHveb5O-r1XKUG23lU_a_mZ6!lBI>FY14<0_d8E0`1596L~hKUc=xj z4jsLWxJ#t0&SL$7F6IG+{_Yb;=MN?@npWgz{L`KN^wcKvoW}$Af)X>AI8-^a<+Rfgn2G1I@0u%j`<5F5o!NiI*yf zCpYk#`?~b>71~Im6q-9TrdA{6$8$iCfI#;L(+e>?R!^r@-)3xXMl6k{Ne z&)gNTi6~x#7KeqseM(9m{ ztiN<2iwBY&Q)=9`ii1E*$;@wRV1sqNxD)!GUX{y;4S(b1`zf#;xnk=i1P1Y40D=D} z{#+M8de&Uj{VqGl5ZxnVk%#oYnpkB@G7gx;=^O)&)LQZ>xR5Gs_l$`hT)xT!eI6Ii zMff&g_7$LaGkenERU`!J>jG%ZYtIql+FDnq_!c0#^tC2LlT;|0FK*?cG7 z)yM++nh*RuH;Ghehr}~yErJ9+%by>Ithnla4k7=dK%Eun&X2XsztF6Q#<%s|(OSbR zB7j$zyn;Ybu$nFn{D4>T3i=Vs03vN+1-$YP-FFD>m;c=J(ZOxU(1{yMpWgXRU4RQW zIZ~%$O|GSc_r3pmL#$qaD-t6*Q%)k5!X=u4pPMUw4txL7Qa^%;Be0*!sej%PsnZ6M zrGF(=6R{0S*Ne?xnZ==DK^ufYgGhIPD0t=AANVWg|Bsx^$YoA;`3U@QA!BeELEZ?6 z>bOa?+jsV#)%y%+zv-zRlJ=+3DjABY>e;VuV<>5Ulqm#@6Dhn5B|#kJLad0xH=(Pu zUlIG)fR@KT+ANr9=-2r)S`*s;Qt|7btI36F+nt$U1l?s0esSHIQf~Amo;q^+fFT-h zI(_G_e8b<6ZQ^SQuzym?DOtjHhE89KAofb5&}UkWT^1AJ-iyyojM#`5v{g6|Vx)HhKV^1e%&?T@V>Hw34Qj48 zdugoxavX+|Q@Ncn$Nrlnhomzz!;@CF&2T>APmE%ZVFpTB=Iizd#GiIaWloph+qz$G?+-;cpSB%#mP4$6pD$@6+`kRFEhVwTwnRR?J2J6 z#4ViAae(tfAHcyxs}^!B1yO0X>zlOe^dew?tt&1fZi^^rl9H;SkH(ZQ%X@^*zI`GN zY5wtg%jzA0YTvO$bqQPmh7wpNo=6Y8|N31a;80LBL&nCQ@?50Axy`dF7K&q_aC{*| zMF_EumwsYk6WPfM181xD+cqZof~nFqJ|aZQV!eL`KAiLh{w4?64_W@X3MGK7B%Q=y zcm{jo5G`TkJO)o+#~Gttq-8%`Rg5sHFC58w~sottIU$qsuIsNq=ClP{khu_ zIso$=BtVD1a$Fw8X&r&X(jT^hnC8J36$QjfUM6?~B^W*}0hCJ-^+E%qUHA8XW8!e# z;!rjRo#`+de?^!@3uz_qte>rfZ#zE2;KYhFcCLC37!ex!$Q%0UZg>JRMJLh{6l8xX zKgI=V9qWdWnV_f4&u#31BN3q@f`E!ErVQ}x?C0%2n-+d8d`Qa8`8D}W1^Rp)ePGMB z2Bber1Ot0c8U&*oFiRc==Z*ESKn554xExQ_*}9hL{DMLdA1XJM(^sm z*oavQQfoqZ@To*}Vk<4_%4$4{xN(q5EeBg(_7?+fU2^>ZLSSbkGne0~=25>L)P)|6 z+RGwPkznn2OwvALdpz&AkcBLQ~5fCv<0f>~aO&S8v7>e}e#+ zr*0$3qDA-!YB;Pcf6gTPsw_%@{c;t;dmuHj&sHs0ZT*^d5(0sX-}^O@hT9X|o>p5p z5d^{HGZHkvHW5@^MK96>aVZt!wk&eKhxSVX!FQVv%ms6Ts{r|wg(K+Mc2P4d+OVT! z3w1vQ&;qhR5=3q(7IVXDMfh=GR*Lc-LRbn0#~Gx!Y7y{fd;!j9j8I5d=&5zBJ_%)& zm|I+Oo16&u*Qh<%a8(SYu<=~gYNCTaG`88e@eB!;hb`t{>VN*$q1F6C1WnTO`EpiA z4Nx=`{mq-hS#&orE|+K3biaX~%YPjDs!)Q&)i|q6+MNtpS~=mqf5E6oOGYJJ$|6+s z#5-G?90A4TWKJWzhD1+v<7s&K7W^J%Tz%)j!3^g&L8uQB=3`lYu{w)Cv_IxgrxhVb4jpEBiFtoZXNA}fcDW|^(Mk)~}ydz=Z2IB&CG!#_B?PTRM7wV+1fr zVjLQrB*R@VK!VQ*CMt~(W?$vsK5-m<9GacKxyBjXK^oX|8%jQ&B93!^gO!&tSP^(Y z^oTI8s|&*5j;LtnB3FKZHsMLMDF$;{0lmE=7T0>*T4!yKLwemE+(?=aps(M9zC5j@ zZ-J+m&AQrTsZcwQIUfXfPA zy*o_Nj?BrjOF8!2(t&Fq^knaE;21B)RRC)y*rk!OCY*Y=18kGy*tuW8{k#C@D}im6 zTsKCTDp0&W0=BvB7C6BRAygYdUfA&}%S)uY#kDyI*l{J^NMs@vBPa*-=Oo7|wbff= zoFNA6+1~}zU%*>{UHK$*-}1eaea|FT;L`XDnmd1*4D!)BUeZ{zQQKnOWZNh>%qJ+r zdFY{3&qM~@14birvWyn2S(a)SJ=WR$GvG5&(1wUCMxFk*Rsrd3w*lu9KRBPq#&G?} z=uegfM$H!w@_88^H0`pIYl!$i9ea>GrQ`j1*hqByQ1;Vkyw$yr zh&(8$myee1X_cB|85z`OosHh}QV%VWwU8-(t2V+wVWJ;M;B<}{$$}xa)w`?$JpYfR0b!)h^2-LeB^U~YEWC~|G<;`FKz3y^^MV6Z< zcFZ8fZ2++wh;*6_e8Qz!UEx7e0&hpyNN~SC&t{autN4@1I4;KqFG!*#A5Z!^Aft^I zgES-ax36YC)*Stw);Cj9VLax--KnLzDdHq;sJ%VN2^VFQrG5I~%t0@eZajL@3 z6G`gga#nebz>QXnFMVV3}4VZ$O*Y~uFc|rJO0b1DR(L3TeUIEYJ#p~N6xzkkV&{UB0oz#(E-AuYgnTKqN$KNhEjc|^l@-Tl|X11O13;Mj=$ zyP^;rjH_n8V&m$=w963mW3wenA#3sHC3>iE`TW16k)qZsEo1+OT64?66#~8%*p;(c z;j6h8(9-64LD@{kJz7#+hCjUGfh3mA zkx4gMF1ImwC{7&ZR&-bP7c7j6#B#LzeD@^WpmH4D<0WQ^MCf(qM>OGdPmr|me9eat z%>9OxR``p`F*M#w+7h8WaTS<@>R&V0A1F8k+_v#;CJ22(>P7DvfycL2G+Q=+DnZ>y z?;3@9v}Yj5^3<0b#zyGG9oruZ{?ZeS3xy&IIEbR3N(lP`3KU)HMSA%jH?$?NdHisphLZBXxMuRg<$OlQ+1~WmA zz4EBniCwGAI&Lc_rKkHTp?^pgjC?3bA@i;8WHJ|w&aVhdy!@BMnf)en`q=Jte>dmK z&jA{s$_R5qt5$|c-`86$#fVc1e6{|?i@C{aMU~zyTXyK~lfe{2He%uMrQ56XuDE54 z=wjGiNJs!!hJu)cQiN*8uSe@HnG&OCjzeKH!0UX}T@#O_;W1L9{0T6rEHnt7S4xO2 zMPg+gT8>!>%V@P(+k|7pAG`!vt#?!RVk6EJwjvW`=f!(Y5ogMQ1+D#TgM8r;Qnf9% z;(GpU-9M{C6jk=OXNh}wDE3}JMNoX>Ql08m^^)zQV)^V`K{J4*T0b zh?=Nv-dpm(h;K~j0OGxMS3$#!=7i|KExSo!J${zs?ys@`U(~6?ks>({W}e!Zb`OJN zQaKihM2;7!Kn!#nJ(2-AiGMWcpivR1_%3bGQ?Rx5+Ap=ixDb@<*-QL)5ogDUqx8qc z+5W8+NG5i}+6kFV1}D}pI{a^0iAxE@WdCi69U}Du z#kID+|33lw|7mFoU3MJ}EKU>cLUZUN5zsPDK#qc4v{|kFKz67yx&z%Tp^QhgLt_@>~EX3md z2t&aO=ps}l5u8m8n!Xz21dkxX0n9p}H~;x=N$~F8xLqiPj(36x)^vOT3XnnGG5?e~ zBFJxpH#9^FBC}?o7mvOLq!s>Ac+-! zxvrs#0Prn}T2k9qx8jkM`ON(<>u*&*oV^p$#Jy%i82Owd6ji8VS^J-x#wG4!@ZXTm z#x))`BY$(F-Fyw@8C9mb2d};&n?2003SU11mZ>-^gGfk`n%HClFO8(A=B@{chTCnV zUyRDQz}KUfIVA%An2(Z2)peB_A!DTb zhCn@qzgg33S`0Fq?OoSCG}@x^9(g*eofu$zZiBW2M?JG-#sI@A_+lObf%)bfsfT2J zK?VY1UKw&D{w$)vXb$?&D)j(i)>!vz0xjfj?%aM z{nZCCMRPR%tkhPdW$b5uwZH7fzSKkv<u?$om-nZ$_YMukopWg+d?0vp0 zYjD~C?8iUKw-D(8n11+2vn7!-3-wM{ZkzQHB~it-^!EUCaYO zuKyP7P5dncrCz3-g2e&;X6wc^;uQvx=)PSwv{)Kfi#+`V!##CJxToN-K_1EL^ps=g zCWR-g(1s7LuGv2LmYHN57YRN&DGc&->o>y)5<#R(K!17_6LOJVDy+OeSEYgSbT$={gk`b|eyBkWTID2LB{ZTr#=lv+_F<8+;0z8o%*8RTg4 z67Ow#)%WJG&przE%s-nxfGOk`hi<8l8i1634m~Lyg#ge{z>N)O-Twm&MBM$;3~>vW zm<=`*H)gSMb%aInUmwGScO!$0@;|Lt<|IAJ)F0jWs+I}x6$*b0!J**zbbi9r=$+0S zv5CR;l?JL&)e3?e>nEwDUwwC=_MPjerT9%njVvRP__dw=uX)z8c#R zqJ~ccjjwld3-oz+q3Uj4`pUW*lU&K{GNq;j?^)m88Fl!_=WQjo=H=+a-KTQAdcV_q z{Xti%*9r08$OUz0AFh@Fv1hrQp-uI)1lpqG);xS_1kX0$<^RB$to|zn?(U+Eym#qv zhiv0{#Sij`5mPWc;cWoAxEV|!X;Gto;K3~VlFL}ND;|G}0LHdh9y8O_FVU|QtlOgbp%r2o%%VK_X{x(xfbb_=RN)60(b9ZBI$y4qy_$Moi6{Z){=k+ zgx$NDNcSaHDk8DlFHItsV&zh@#t%QGeKo{8bvHP~FKDAEs`3d4h)`%P<{J|8#h~&? z`+;M_ia^JMFEB>gS23=_&dhH&;aBmgo$`T5`h#!OtY?MV#?m9#SW+z%iX!%>?dny4 zb6ku@6R-GPTx&&6;-Jany#mOW*1aM^iJe^*YkEbl@^V3{rxuaXFZgtkb7)$b9;nI$ujxI1UQeX;VTFic7P{5UP9g zfo}D~MX6^8(X-JX+01{KeHQMr?GiMgdkdn8aepKr9aNxlU{N$wgIw-K&;I?%m|^N>_pQ>bmuR5+oEvArMU z<69#Mtw0i5&t6$&u7E~;kIrwtn`-&$Ad2EKe;%`KSN}{(PWjb&$6iUpxVSy-+G|9Ow*XFY8 zW!V1LyC@Ra@En7IjHL1qY=kRCW4~JZ)_i1!lrP^#hBuYW)aJ?;X?BNEI6jwC;z*ba z7$kYHCOsz-Lus72#>e|~8wwY$Df!od0DteQp4E%2y-)vQ5-Vgel05hK&wnu{i4CZD z%&ij$3gZ$F04!eIo-2ePu80D_R}3!eCW0Kpd<9E>rMZJ?R#3Q6a|>c2Lz|weX`?2E zBq;HQ+{-o2n3bdmRIs+Mm`gX&k2^J9vXK>f;&pgCVzqG1o}m2|IR|e+iMe+yI9KDv z^naCh_3==pd;FQ>jK)kfLd2FSThk)k)@nB^C#f)LWJx61T9RDG7LqdO6iHNjd(jKa z)Y{gT&{feQC#~f6LTH;pMr!MNTgltRIQ#UupSyRS;lDe7&F6ETXP%ee_xJt1Jilj- zO?_q17j*V39vGtKDDLop>3ndt$#Xlx>D1w^V6{1pk=u+nAP|yyQ8wk#VSc z78g(}*|YbP!CeLb-+u7h|B}ytwvo_-Qq}X_f|B)SEX_$P_=-`E!xf;g7v`vERA40~ z{ysUI#s7X4;8!f~RCh@s2y`H@b@;mInQSzk@!kqV;}@*-!Rj(Odd=@7H_)l@YTx}3 z9$2JhhFZ0MalcgUc&ZFP;|aV(KQ*B#TcJ)V7JSxyqT2ab_Ps3j;ME0Q2ZB$F+7VHb z1P)i6WRr*1fgIzG-QNLLW@Ds&sxE>s_?ko5A;IYy%4z~Th@f=02$WTyvA^U(TRYOh0TOpInzrQT{M1P2dY7gs}nj& z!G35#wJvIwRvJ>{=lCq*#F0Ey|2g0A-^{*a2)>~|L%GD-s($a)?lJwX#=y1v&{+g5 z3dn++jC-EHe29_ro5PWc`Sm3Pv%9q`C!hiZdsI81>#IKv9X?Io>6_q6 zcfz!sUF10s9I=_FuF>$2cn!1)fue<|t>mObbGXG|12S8Z*a7X{?*ZW&w=#pR1;8Xz zj;6`*v(R+fKk?=vNQXBqAQNheQc#}-mvbW{09EIKCsPl=0nZ(Lf=WiV48mu>=d}-i zE~v({lP>%{JJFw+?p+97u7IXpNc*BvVz{eX#PNf24ip(`WDgp69MF(ys==m7a8XIy zQ}?Nl3*F}3a6-GkvOrWGP!c%#2^__DvxQ+pR5%g|^96h;27bXjF0xych#eWUafwU9 zC=WdA`q@a`wgnGjs=-d3mEix@uje8gXp}y&NmLBOri+(EyG} z@u2= zqqW$R0DA^C&)jJNxZB{p9}yzmfJJJJ6lr1PG^cV@HY;(L=i2qQak>z?|IlhiYs_SN z)Q^h*i)1Mt(0P=1mgbUUEXUZQ_<%?gdBu`@Eg(m4X zz=Yg|PIQo$wOh$F48J;cVg@+T9YkG|~_CKn{ z!K&0fXksQ;%cE&bbLOZ@}SAI3=6?+&2C zHNi%0m^NR(eg{%@%f)Z3P@e;0SCJEf$_IW_#-pAzv-f2w`WctsZF~Cf`)Kt(zKGk> zs*rYb$@F5W-N4IY?bxlV<57u43H`)9fy{0FYDG42%@6{)`FGCP`NJVE*Czu(%EiAS$Q4|t#WzpZZG6*bNw&P}G1~-+#CKwm zvBK8L28>3^ltYX!Io33Z&J$Q`M+sBb*k6_~XkHRxJ&<^WsXG&Wq4pdb9Jh8rRFAla zsfIooORlRfX*_v%=Fp($Bm?@%`f8;(jH)KMABQ;IHiMPUOl9R%+1;yi=Ieu`u>Erp z-j?OkLwe*jl>x}V-0>S)^)ao4DePJbSfmzGaD}h4EGp;%IDoGiTlnF9VSjKGir_XoB%e-Vb6}NR4QbG4!%*UKD?- zwr4oU6(*yMem)ETrWR}-T#lZ3NhrE!knOnm<|QC|tGz*^pjHlZX&+7bYx_TZsZXcN zPCc-mr*y$Vt-G!pGCckQj{joNbp_Hza)C?%y(tEuyW}krgK~W4!<^8l*?b_6>X`LE zF~i<@Zi>sJ%(Pe$kbGB@iTs>b8hMYI7UuyZ@)`|mm`XqJJ8Aff*BOjVR(PUd+r|!L zN#tKm(u*QTnWKx{fu!ZG5W%jXly;m3zI--^MX#M}j^KfM12b>V&*773iyZU`jWyq6 zq6)Q)O>*rgiD4faNR-UBNQ`XE+H*)9+&IRKs8kLEy3zFAz}2Hljf<@Zg?7WuOmAb| zAI6WQs*v*Pj|;*|erqji6{;?@RrG0G>T3G9PnBjr%#`psQ^g}GdkrVLtCsk6@i7g2Wd^r)~agj<{$)V^LgYmBo*kHMSA)<4Fa^hhWBlh2u zN+=o(#@m;I(Fv z4wu&?X_J0^%)e0wghngv)OG^rSkLL}jmms5BxaOxH%Xw_7EzD*srVapUl&1DIUw7JS7W8-ZG#MftcnavtI3gE|m#TwTFm*9PW0#u*>BLDyZ literal 0 HcmV?d00001 diff --git a/client/src/main/resources/shader/model/fragment.glsl b/client/src/main/resources/shader/model/fragment.glsl new file mode 100644 index 0000000..5235c00 --- /dev/null +++ b/client/src/main/resources/shader/model/fragment.glsl @@ -0,0 +1,20 @@ +#version 460 core + +in vec2 textureCoords; +in vec3 vertexNormal; + +out vec4 fragmentColor; + +uniform sampler2D textureSampler; + +void main() { + vec4 baseColor = texture(textureSampler, textureCoords); + vec3 lightDirection = normalize(vec3(0.5, -1.0, -0.5));// TODO: Add this via a uniform. + vec3 lightColor = vec3(1.0, 1.0, 0.9); // TODO: Add this via a uniform. + + vec3 ambientComponent = vec3(0.1, 0.1, 0.1); + vec3 diffuseComponent = max(dot(vertexNormal * -1, lightDirection), 0.0) * lightColor; + // TODO: Add shading based on light. + // fragmentColor = vec4((ambientComponent + diffuseComponent), 1.0) * baseColor; + fragmentColor = baseColor; +} \ No newline at end of file diff --git a/client/src/main/resources/shader/model/vertex.glsl b/client/src/main/resources/shader/model/vertex.glsl new file mode 100644 index 0000000..9dad643 --- /dev/null +++ b/client/src/main/resources/shader/model/vertex.glsl @@ -0,0 +1,18 @@ +#version 460 core + +layout (location = 0) in vec3 vertexPositionIn; +layout (location = 1) in vec3 vertexNormalIn; +layout (location = 2) in vec2 textureCoordsIn; + +uniform mat4 projectionTransform; +uniform mat4 viewTransform; +uniform mat4 modelTransform; + +out vec2 textureCoords; +out vec3 vertexNormal; + +void main() { + gl_Position = projectionTransform * viewTransform * modelTransform * vec4(vertexPositionIn, 1.0); + vertexNormal = vec3(modelTransform * vec4(vertexNormalIn, 1.0)); + textureCoords = textureCoordsIn; +} \ No newline at end of file diff --git a/core/src/main/java/nl/andrewl/aos_core/ImageUtils.java b/core/src/main/java/nl/andrewl/aos_core/ImageUtils.java index 31f6d34..35f70a2 100644 --- a/core/src/main/java/nl/andrewl/aos_core/ImageUtils.java +++ b/core/src/main/java/nl/andrewl/aos_core/ImageUtils.java @@ -16,7 +16,7 @@ public class ImageUtils { // ARGB format to -> RGBA for( int h = 0; h < height; h++ ) for( int w = 0; w < width; w++ ) { - int argb = image.getRGB( w, h ); + int argb = image.getRGB(w, h); buf.put( (byte) ( 0xFF & ( argb >> 16 ) ) ); buf.put( (byte) ( 0xFF & ( argb >> 8 ) ) ); buf.put( (byte) ( 0xFF & ( argb ) ) ); @@ -25,4 +25,14 @@ public class ImageUtils { buf.flip(); return buf; } + + public static BufferedImage rotateClockwise90(BufferedImage src) { + int w = src.getWidth(); + int h = src.getHeight(); + BufferedImage dest = new BufferedImage(h, w, src.getType()); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + dest.setRGB(y, w - x - 1, src.getRGB(x, y)); + return dest; + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java index ef16e08..df8eaf7 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java @@ -1,7 +1,6 @@ package nl.andrewl.aos_core.model; import net.openhft.hashing.LongHashFunction; -import org.joml.Vector3f; import org.joml.Vector3i; import java.util.Random; @@ -115,6 +114,11 @@ public class Chunk { return sb.toString(); } + @Override + public int hashCode() { + return position.hashCode(); + } + public long blockHash() { return LongHashFunction.xx3(0).hashBytes(blocks); } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java index 07d10d2..1e927f9 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java @@ -21,4 +21,12 @@ public record PlayerJoinMessage( player.getOrientation().x, player.getOrientation().y ); } + + public Player toPlayer() { + Player p = new Player(id, username); + p.getPosition().set(px, py, pz); + p.getVelocity().set(vx, vy, vz); + p.getOrientation().set(ox, oy); + return p; + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java b/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java index aed09fe..e73d6df 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/UdpReceiver.java @@ -35,7 +35,6 @@ public class UdpReceiver implements Runnable { handler.handle(msg, packet); } catch (SocketException e) { if (e.getMessage().equals("Socket closed")) { - System.out.println("Socket closed!"); break; } e.printStackTrace(); diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java index 2796b7c..0aa5ded 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/ChunkUpdateMessage.java @@ -28,4 +28,12 @@ public record ChunkUpdateMessage( world.getBlockAt(worldPos.x, worldPos.y, worldPos.z) ); } + + public Vector3i getChunkPos() { + return new Vector3i(cx, cy, cz); + } + + public Vector3i getLocalPos() { + return new Vector3i(lx, ly, lz); + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java index 9584fc0..803540b 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java @@ -1,5 +1,6 @@ package nl.andrewl.aos_core.net.udp; +import nl.andrewl.aos_core.model.Player; import nl.andrewl.record_net.Message; /** @@ -12,4 +13,11 @@ public record PlayerUpdateMessage( float vx, float vy, float vz, float ox, float oy, boolean crouching -) implements Message {} +) implements Message { + + public void apply(Player p) { + p.getPosition().set(px, py, pz); + p.getVelocity().set(vx, vy, vz); + p.getOrientation().set(ox, oy); + } +} diff --git a/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java index b2caa60..5e9fce3 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java +++ b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java @@ -91,6 +91,13 @@ public class ClientCommunicationHandler { log.debug("Sent connect accept message."); sendTcpMessage(new WorldInfoMessage(server.getWorld())); + // Send join info for all players that are already connected. + for (var player : server.getPlayerManager().getPlayers()) { + if (player.getId() != this.player.getId()) { + sendTcpMessage(new PlayerJoinMessage(player)); + } + } + // Send chunk data. for (var chunk : server.getWorld().getChunkMap().values()) { sendTcpMessage(new ChunkDataMessage(chunk)); }