├── client ├── options.txt ├── pom.xml ├── resources │ ├── assets │ │ ├── audio │ │ │ ├── dig.wav │ │ │ ├── place.wav │ │ │ ├── pop.wav │ │ │ ├── snap.wav │ │ │ └── throw.wav │ │ ├── fonts │ │ │ └── default │ │ │ │ ├── info.txt │ │ │ │ └── sheet01.png │ │ ├── models │ │ │ └── test.glb │ │ └── textures │ │ │ ├── blueSquare.png │ │ │ ├── clouds.png │ │ │ ├── clouds_online.png │ │ │ ├── cursors │ │ │ ├── cursor.png │ │ │ ├── mallet.png │ │ │ ├── mallet32.png │ │ │ ├── pickaxe.png │ │ │ ├── pickaxe32.png │ │ │ ├── pickaxeBorderless.png │ │ │ └── pickaxeBorderless32.png │ │ │ ├── dirt.png │ │ │ ├── glass.png │ │ │ ├── grass.png │ │ │ ├── greenArm.png │ │ │ ├── greenFace.png │ │ │ ├── night.png │ │ │ ├── redSquare.png │ │ │ ├── stickman.png │ │ │ └── stickman_crouch.png │ └── shaders │ │ ├── new │ │ ├── blocks.frag │ │ ├── blocks.vert │ │ ├── fullscreen-blend.frag │ │ ├── fullscreen.frag │ │ ├── fullscreen.geom │ │ ├── fullscreen.vert │ │ ├── light.frag │ │ ├── light.geom │ │ ├── light.vert │ │ ├── model.frag │ │ ├── model.vert │ │ ├── shading.comp │ │ ├── shading_apply.frag │ │ └── shading_apply.geom │ │ └── old │ │ ├── model.frag │ │ └── model.vert ├── src │ ├── de │ │ └── matthiasmann │ │ │ └── twl │ │ │ └── utils │ │ │ └── PNGDecoder.java │ ├── module-info.java │ └── ritzow │ │ └── sandbox │ │ └── client │ │ ├── GameLoop.java │ │ ├── GameState.java │ │ ├── InWorldContext.java │ │ ├── MainMenuContext.java │ │ ├── StartClient.java │ │ ├── audio │ │ ├── AudioData.java │ │ ├── AudioSystem.java │ │ ├── JavaxAudioSystem.java │ │ ├── OpenALAudioSystem.java │ │ ├── OpenALException.java │ │ ├── Sound.java │ │ └── WAVEDecoder.java │ │ ├── build │ │ └── CompileShaders.java │ │ ├── data │ │ ├── ClientOptions.java │ │ ├── StandardClientOptions.java │ │ └── StandardClientProperties.java │ │ ├── graphics │ │ ├── BlockRenderProgram.java │ │ ├── Camera.java │ │ ├── Display.java │ │ ├── Framebuffer.java │ │ ├── FullscreenQuadProgram.java │ │ ├── GLTFModelLoader.java │ │ ├── GameModels.java │ │ ├── Graphics.java │ │ ├── GraphicsUtility.java │ │ ├── Light.java │ │ ├── LightRenderProgram.java │ │ ├── LightingApplyProgram.java │ │ ├── Lit.java │ │ ├── Model.java │ │ ├── ModelDestination.java │ │ ├── ModelRenderProgramBase.java │ │ ├── ModelRenderProgramEnhanced.java │ │ ├── ModelRenderProgramOld.java │ │ ├── ModelRenderer.java │ │ ├── ModelStorage.java │ │ ├── MutableGraphics.java │ │ ├── OpenGLByteTexture.java │ │ ├── OpenGLException.java │ │ ├── OpenGLTexture.java │ │ ├── RenderManager.java │ │ ├── Renderable.java │ │ ├── Shader.java │ │ ├── ShaderProgram.java │ │ ├── ShadingApplyProgram.java │ │ ├── ShadingComputeProgram.java │ │ ├── StaticLight.java │ │ ├── TextureAtlas.java │ │ ├── TextureData.java │ │ └── Textures.java │ │ ├── input │ │ ├── Button.java │ │ ├── Control.java │ │ ├── ControlsContext.java │ │ ├── ControlsQuery.java │ │ ├── InputProvider.java │ │ └── controller │ │ │ ├── InteractionController.java │ │ │ └── TrackingCameraController.java │ │ ├── network │ │ ├── Client.java │ │ ├── GameTalker.java │ │ └── ServerBadDataException.java │ │ ├── ui │ │ ├── Animation.java │ │ ├── Circle.java │ │ ├── Font.java │ │ ├── GuiElement.java │ │ ├── GuiRenderer.java │ │ ├── Position.java │ │ ├── Rectangle.java │ │ ├── Shape.java │ │ ├── StandardGuiRenderer.java │ │ └── element │ │ │ ├── AbsoluteGuiPositioner.java │ │ │ ├── BorderAnchor.java │ │ │ ├── Button.java │ │ │ ├── EditableText.java │ │ │ ├── HBox.java │ │ │ ├── HBoxDynamic.java │ │ │ ├── Holder.java │ │ │ ├── Icon.java │ │ │ ├── RotationAnimation.java │ │ │ ├── Scaler.java │ │ │ ├── Text.java │ │ │ ├── TextField.java │ │ │ ├── VBox.java │ │ │ └── VBoxDynamic.java │ │ ├── util │ │ ├── ClientUtility.java │ │ └── SerializationProvider.java │ │ └── world │ │ ├── ClientWorldRendererLightmap.java │ │ ├── ClientWorldRendererModelOnly.java │ │ ├── ClientWorldRendererTraced.java │ │ ├── block │ │ ├── ClientBlockProperties.java │ │ ├── ClientDirtBlock.java │ │ ├── ClientGlassBlock.java │ │ └── ClientGrassBlock.java │ │ ├── entity │ │ ├── ClientItemEntity.java │ │ └── ClientPlayerEntity.java │ │ └── item │ │ └── ClientBlockItem.java └── test │ └── ritzow │ └── sandbox │ └── client │ └── test │ └── RunTest.java ├── clientlauncher ├── Linux │ ├── Sandbox2DLinuxLauncher.vcxproj │ ├── Sandbox2DLinuxLauncher │ │ └── Sandbox2DLinuxLauncher.vcxproj │ └── main.cpp ├── Shared │ ├── shared.cpp │ └── src │ │ └── ritzow │ │ └── sandbox │ │ └── build │ │ └── Build.java ├── Windows │ ├── Sandbox2DLauncherWindows.vcxproj │ ├── launch.cpp │ ├── launch_static.cpp │ └── resources │ │ ├── greenFace.ico │ │ ├── resource.h │ │ └── resource.rc ├── build.ps1 ├── macOS │ ├── prepare-build.bat │ └── src │ │ ├── launch.cpp │ │ └── run.command └── pom.xml ├── pom.xml ├── readme.md ├── server ├── pom.xml ├── src │ ├── module-info.java │ └── ritzow │ │ └── sandbox │ │ └── server │ │ ├── CommandParser.java │ │ ├── SerializationProvider.java │ │ ├── StartServer.java │ │ ├── network │ │ ├── ClientBadDataException.java │ │ ├── ClientNetworkInfo.java │ │ ├── ClientState.java │ │ ├── GameServer.java │ │ └── Server.java │ │ └── world │ │ └── entity │ │ └── ServerPlayerEntity.java └── test │ └── ritzow │ └── sandbox │ └── server │ └── test │ └── RunTest.java ├── serverlauncher ├── pom.xml ├── prepare_build.bat ├── resources │ ├── greenFace.png │ └── ui │ │ ├── launcher_main.fxml │ │ └── style.css ├── run_server.bat └── src │ ├── module-info.java │ └── ritzow │ └── sandbox │ └── server │ └── launcher │ ├── LauncherController.java │ └── StartLauncher.java ├── shared ├── pom.xml └── src │ ├── module-info.java │ └── ritzow │ └── sandbox │ ├── data │ ├── Bytes.java │ ├── DataReader.java │ ├── Deserializer.java │ ├── SerializationException.java │ ├── Serializer.java │ ├── SerializerReaderWriter.java │ ├── Transportable.java │ ├── TransportableDataReader.java │ └── TypeNotRegisteredException.java │ ├── network │ ├── NetworkUtility.java │ ├── Protocol.java │ ├── ReceivePacket.java │ ├── SendPacket.java │ └── TimeoutException.java │ ├── util │ ├── Optimized.java │ └── Utility.java │ └── world │ ├── BlockGrid.java │ ├── World.java │ ├── block │ ├── Block.java │ ├── DirtBlock.java │ ├── GlassBlock.java │ └── GrassBlock.java │ ├── component │ ├── Inventory.java │ ├── Living.java │ ├── Luminous.java │ ├── Physical.java │ └── Positional.java │ ├── entity │ ├── Entity.java │ ├── ItemEntity.java │ └── PlayerEntity.java │ ├── generator │ ├── SinusoidWorldGenerator.java │ └── WorldGenerator.java │ └── item │ ├── BlockItem.java │ └── Item.java └── to-do.txt /client/options.txt: -------------------------------------------------------------------------------- 1 | #Options file 2 | 3 | use_new_opengl = true 4 | vsync = false 5 | fps_print = 6 | fps_limit = 7 | use_internet = false 8 | gui_scale = 500 9 | debug = false 10 | debug_opengl = true 11 | lefty = true -------------------------------------------------------------------------------- /client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | net.ritzow 6 | sandbox2d-parent 7 | ${revision} 8 | 9 | 10 | sandbox2d-client 11 | 12 | 13 | 14 | 15 | org.lwjgl 16 | lwjgl-bom 17 | 3.3.1 18 | import 19 | pom 20 | 21 | 22 | 23 | 24 | 25 | src 26 | test 27 | 28 | 29 | resources 30 | 31 | 32 | 33 | 34 | org.apache.maven.plugins 35 | maven-surefire-plugin 36 | 37 | 38 | true 39 | true 40 | true 41 | true 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | net.ritzow 51 | sandbox2d-shared 52 | 53 | 54 | org.lwjgl 55 | lwjgl 56 | 57 | 58 | org.lwjgl 59 | lwjgl-glfw 60 | 61 | 62 | org.lwjgl 63 | lwjgl-openal 64 | 65 | 66 | org.lwjgl 67 | lwjgl-opengl 68 | 69 | 70 | org.lwjgl 71 | lwjgl 72 | natives-windows 73 | 74 | 75 | org.lwjgl 76 | lwjgl-glfw 77 | natives-windows 78 | 79 | 80 | org.lwjgl 81 | lwjgl-openal 82 | natives-windows 83 | 84 | 85 | org.lwjgl 86 | lwjgl-opengl 87 | natives-windows 88 | 89 | 90 | org.junit.jupiter 91 | junit-jupiter-api 92 | test 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /client/resources/assets/audio/dig.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/audio/dig.wav -------------------------------------------------------------------------------- /client/resources/assets/audio/place.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/audio/place.wav -------------------------------------------------------------------------------- /client/resources/assets/audio/pop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/audio/pop.wav -------------------------------------------------------------------------------- /client/resources/assets/audio/snap.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/audio/snap.wav -------------------------------------------------------------------------------- /client/resources/assets/audio/throw.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/audio/throw.wav -------------------------------------------------------------------------------- /client/resources/assets/fonts/default/info.txt: -------------------------------------------------------------------------------- 1 | Default Font -------------------------------------------------------------------------------- /client/resources/assets/fonts/default/sheet01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/fonts/default/sheet01.png -------------------------------------------------------------------------------- /client/resources/assets/models/test.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/models/test.glb -------------------------------------------------------------------------------- /client/resources/assets/textures/blueSquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/blueSquare.png -------------------------------------------------------------------------------- /client/resources/assets/textures/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/clouds.png -------------------------------------------------------------------------------- /client/resources/assets/textures/clouds_online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/clouds_online.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/cursor.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/mallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/mallet.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/mallet32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/mallet32.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/pickaxe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/pickaxe.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/pickaxe32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/pickaxe32.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/pickaxeBorderless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/pickaxeBorderless.png -------------------------------------------------------------------------------- /client/resources/assets/textures/cursors/pickaxeBorderless32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/cursors/pickaxeBorderless32.png -------------------------------------------------------------------------------- /client/resources/assets/textures/dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/dirt.png -------------------------------------------------------------------------------- /client/resources/assets/textures/glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/glass.png -------------------------------------------------------------------------------- /client/resources/assets/textures/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/grass.png -------------------------------------------------------------------------------- /client/resources/assets/textures/greenArm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/greenArm.png -------------------------------------------------------------------------------- /client/resources/assets/textures/greenFace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/greenFace.png -------------------------------------------------------------------------------- /client/resources/assets/textures/night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/night.png -------------------------------------------------------------------------------- /client/resources/assets/textures/redSquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/redSquare.png -------------------------------------------------------------------------------- /client/resources/assets/textures/stickman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/stickman.png -------------------------------------------------------------------------------- /client/resources/assets/textures/stickman_crouch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/client/resources/assets/textures/stickman_crouch.png -------------------------------------------------------------------------------- /client/resources/shaders/new/blocks.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | //If I ever want multiple outputs: 4 | //https://stackoverflow.com/questions/30025009/in-opengl-is-it-possible-for-one-shader-program-in-one-draw-call-to-render-to?rq=1 5 | 6 | layout(location = 0) in smooth vec2 textureCoord; 7 | 8 | layout(location = 0) out vec4 color; //color attachment 0 9 | 10 | layout(location = 2, binding = 0) uniform sampler2D atlasTexture; 11 | layout(location = 3) uniform float exposure; 12 | 13 | void main() { 14 | vec4 texel = texture(atlasTexture, textureCoord); 15 | color = vec4(texel.rgb * exposure, texel.a); 16 | } -------------------------------------------------------------------------------- /client/resources/shaders/new/blocks.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in vec2 position; 4 | layout(location = 1) in vec2 textureCoord; 5 | 6 | layout(location = 0) out smooth vec2 outTextureCoord; 7 | 8 | layout(location = 0) uniform mat4 view; 9 | layout(location = 1) uniform uvec3 blockGrid; //left block X, bottom blockY, blocks width 10 | 11 | const int MAX_RENDER_COUNT = 100; 12 | 13 | layout(binding = 1, std140) uniform instance_data { 14 | int offsets[MAX_RENDER_COUNT]; 15 | }; 16 | 17 | void main() { 18 | //get the index into the viewport block-grid 19 | //of the current series of the same block, then add the index of the current block 20 | int index = offsets[gl_DrawID] + gl_InstanceID; 21 | uint blockX = index % blockGrid[2] + blockGrid[0]; 22 | uint blockY = index / blockGrid[2] + blockGrid[1]; 23 | gl_Position = view * vec4(position + vec2(float(blockX), float(blockY)), 0.0, 1.0); 24 | outTextureCoord = textureCoord; 25 | } -------------------------------------------------------------------------------- /client/resources/shaders/new/fullscreen-blend.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in smooth vec2 textureCoord; 4 | layout(location = 0) out vec4 fragColor;//color attachment 0 5 | 6 | layout(location = 0, binding = 0) uniform sampler2D diffuse; 7 | layout(location = 1, binding = 1) uniform sampler2D lighting; 8 | //TODO layout(location = 2, binding = 2) uniform sampler2D shadow; 9 | 10 | const float pi2 = 6.28318530718;// Pi*2 11 | 12 | // GAUSSIAN BLUR SETTINGS {{{ 13 | const float DIRECTIONS = 16.0;// BLUR DIRECTIONS (Default 16.0 - More is better but slower) 14 | const float QUALITY = 4.0;// BLUR QUALITY (Default 4.0 - More is better but slower) 15 | const float SIZE = 8.0;// BLUR SIZE (Radius) default 8 16 | // GAUSSIAN BLUR SETTINGS }}} 17 | 18 | const float DIVISOR = QUALITY * DIRECTIONS - 15.0; 19 | 20 | void main() { 21 | 22 | //Gaussian blur from https://www.shadertoy.com/view/Xltfzj 23 | // vec2 radius = SIZE/vec2(1920, 1080);//Size/iResolution.xy; 24 | // 25 | // // Pixel colour 26 | // vec4 color = texture(diffuse, textureCoord); 27 | // float alpha = color.a; 28 | // 29 | // // Blur calculations (TODO this needs to eat into the diffuse/shadow textureand make edges more transparent) 30 | // for(float d = 0.0; d < pi2; d += pi2/DIRECTIONS) { 31 | // for(float i = 1.0/QUALITY; i <= 1.0; i += 1.0/QUALITY) { 32 | // //TODO if is completely transparent, don't use 33 | // color += texture(diffuse, textureCoord + vec2(cos(d), sin(d)) * radius * i); 34 | // } 35 | // } 36 | // 37 | // color /= DIVISOR; 38 | // color.a = alpha; //removes soft edges that extend past opaque parts of texture 39 | 40 | //Output to screen 41 | fragColor = texture(diffuse, textureCoord) * texture(lighting, textureCoord); //color * texture(lighting, textureCoord); 42 | } 43 | -------------------------------------------------------------------------------- /client/resources/shaders/new/fullscreen.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in smooth vec2 textureCoord; 4 | layout(location = 0) out vec4 fragColor; //color attachment 0 5 | layout(location = 0) uniform sampler2D source; //TODO maybe use an image instead of sampler to be more efficient 6 | 7 | void main() { 8 | fragColor = texture(source, textureCoord); 9 | } 10 | -------------------------------------------------------------------------------- /client/resources/shaders/new/fullscreen.geom: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(points) in; 4 | layout(triangle_strip, max_vertices = 4) out; 5 | 6 | layout(location = 0) out smooth vec2 textureCoord; 7 | 8 | void main() { 9 | textureCoord = vec2(0, 1); 10 | gl_Position = vec4(-1, 1, 0, 1); 11 | EmitVertex(); 12 | 13 | textureCoord = vec2(0, 0); 14 | gl_Position = vec4(-1, -1, 0, 1); 15 | EmitVertex(); 16 | 17 | textureCoord = vec2(1, 1); 18 | gl_Position = vec4(1, 1, 0, 1); 19 | EmitVertex(); 20 | 21 | textureCoord = vec2(1, 0); 22 | gl_Position = vec4(1, -1, 0, 1); 23 | EmitVertex(); 24 | 25 | EndPrimitive(); 26 | } 27 | -------------------------------------------------------------------------------- /client/resources/shaders/new/fullscreen.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /client/resources/shaders/new/light.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in smooth vec2 inPosition; 4 | layout(location = 2) in smooth vec2 solidTextureCoord; 5 | 6 | layout(location = 1) in flat vec3 inColor; 7 | layout(location = 3) in flat vec2 textureCenter; 8 | layout(location = 4) in flat float sampleScale; 9 | 10 | layout(location = 0) out vec4 fragColor; //color attachment 0 11 | 12 | layout(location = 1, binding = 0) uniform sampler2D solidTexture; 13 | 14 | const float SAMPLE_DISTANCE = 0.001; 15 | const float DISSIPATION_FACTOR = 0.0025; 16 | 17 | void main() { 18 | //TODO still need to deal with aspect ratio issues 19 | //texture coords are fewer in one direction based on aspect ratio (usually fewer horizontally since window is wide) 20 | vec2 increment = normalize(solidTextureCoord - textureCenter) * SAMPLE_DISTANCE; //texture coord distance to increment 21 | int samples = int(distance(solidTextureCoord, textureCenter)/length(increment)); 22 | float alpha = 1 - length(inPosition); //standard alpha without occlusion 23 | 24 | if(alpha <= 0) 25 | discard; 26 | 27 | for(vec2 texCoord = textureCenter; alpha > 0 && samples > 0; texCoord += increment, samples--) { 28 | alpha -= texture(solidTexture, texCoord).a * DISSIPATION_FACTOR / sampleScale; 29 | } 30 | 31 | fragColor = vec4(inColor, alpha); 32 | } 33 | -------------------------------------------------------------------------------- /client/resources/shaders/new/light.geom: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | const int NUM_SIDES = 10; 4 | const float PI = 3.1415926; 5 | const float INCREMENT = PI * 2.0 / NUM_SIDES; 6 | 7 | layout(points) in; 8 | layout(triangle_strip, max_vertices = NUM_SIDES * 3) out; 9 | 10 | layout(location = 0) in vec2 position[]; //calculated in Java 11 | layout(location = 1) in vec3 color[]; 12 | layout(location = 2) in float intensity[]; 13 | 14 | //interpolate relative position, nopserspective since its always a regular square 15 | layout(location = 0) out smooth vec2 outPosition; 16 | layout(location = 2) out smooth vec2 solidTextureCoord; 17 | 18 | layout(location = 1) out flat vec3 outColor; 19 | layout(location = 3) out flat vec2 textureCenter; 20 | layout(location = 4) out flat float sampleScale; 21 | 22 | layout(location = 3) uniform float scale; 23 | layout(location = 0) uniform mat4 view; //TODO dont use a matrix, because rotation is unnecessary and only one scale is needed 24 | 25 | void main() { 26 | //TODO could use polygons to compute fewer fragments https://open.gl/geometry 27 | //TODO could generate a polygon then do the raycasting in here to determine each vertex darkness then interpolate it in the fragment shader (fewer raycasts if done here) 28 | //Generate the actual geometry for the light from the position in world space and light properties 29 | 30 | //set flat values 31 | // outColor = color[0]; 32 | // textureCenter = ((view * vec4(position[0], 0, 1)).xy + 1)/2; 33 | // sampleScale = scale; //length((view * vec4(0, 1, 0, 1)).y); 34 | // 35 | // outPosition = vec2(-1, 1); 36 | // gl_Position = view * vec4(position[0] + vec2(-intensity[0], intensity[0]), 0, 1); 37 | // solidTextureCoord = (gl_Position.xy + 1)/2; 38 | // EmitVertex(); 39 | // 40 | // outPosition = vec2(-1, -1); 41 | // gl_Position = view * vec4(position[0] - vec2(intensity[0], intensity[0]), 0, 1); 42 | // solidTextureCoord = (gl_Position.xy + 1)/2; 43 | // EmitVertex(); 44 | // 45 | // outPosition = vec2(1, 1); 46 | // gl_Position = view * vec4(position[0] + vec2(intensity[0], intensity[0]), 0, 1); 47 | // solidTextureCoord = (gl_Position.xy + 1)/2; 48 | // EmitVertex(); 49 | // 50 | // outPosition = vec2(1, -1); 51 | // gl_Position = view * vec4(position[0] + vec2(intensity[0], -intensity[0]), 0, 1); 52 | // solidTextureCoord = (gl_Position.xy + 1)/2; 53 | // EmitVertex(); 54 | // 55 | // EndPrimitive(); 56 | 57 | vec2 pos = position[0]; 58 | float length = intensity[0]; 59 | vec4 center = view * vec4(pos, 0, 1); 60 | outColor = color[0]; 61 | textureCenter = (center.xy + 1)/2; 62 | sampleScale = scale; 63 | 64 | outPosition = vec2(cos(0), -sin(0)); 65 | gl_Position = view * vec4(pos + length * outPosition, 0, 1); 66 | solidTextureCoord = (gl_Position.xy + 1)/2; 67 | EmitVertex(); 68 | 69 | float angle = INCREMENT; 70 | for(int i = 0; i <= NUM_SIDES; angle += INCREMENT, i++) { 71 | outPosition = vec2(0, 0); 72 | gl_Position = center; 73 | solidTextureCoord = textureCenter; 74 | EmitVertex(); 75 | 76 | outPosition = vec2(cos(angle), -sin(angle)); 77 | gl_Position = view * vec4(pos + length * outPosition, 0, 1); 78 | solidTextureCoord = (gl_Position.xy + 1)/2; 79 | EmitVertex(); 80 | 81 | EndPrimitive(); //End triangle 82 | 83 | EmitVertex(); //Start new triangle 84 | } 85 | } -------------------------------------------------------------------------------- /client/resources/shaders/new/light.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in vec2 position; 4 | layout(location = 1) in vec3 color; 5 | layout(location = 2) in float intensity; 6 | 7 | layout(location = 0) out vec2 outPosition; 8 | layout(location = 1) out vec3 outColor; 9 | layout(location = 2) out float outIntensity; 10 | 11 | void main() { 12 | outPosition = position; 13 | outColor = color; 14 | outIntensity = intensity; 15 | } 16 | -------------------------------------------------------------------------------- /client/resources/shaders/new/model.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | //If I ever want multiple outputs: 4 | //https://stackoverflow.com/questions/30025009/in-opengl-is-it-possible-for-one-shader-program-in-one-draw-call-to-render-to?rq=1 5 | 6 | layout(location = 0) in smooth vec2 textureCoord; 7 | layout(location = 1) in flat float opacity; 8 | layout(location = 2) in flat float exposure; 9 | 10 | layout(location = 0) out vec4 color; //color attachment 0 11 | 12 | layout(location = 1, binding = 0) uniform sampler2D atlasTexture; 13 | 14 | void main() { 15 | vec4 texel = texture(atlasTexture, textureCoord); 16 | color = vec4(texel.rgb * exposure, texel.a * opacity); 17 | } -------------------------------------------------------------------------------- /client/resources/shaders/new/model.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in vec2 position; 4 | layout(location = 1) in vec2 textureCoord; 5 | 6 | layout(location = 0) out smooth vec2 outTextureCoord; 7 | layout(location = 1) out flat float opacity; 8 | layout(location = 2) out flat float exposure; 9 | 10 | layout(location = 0) uniform mat4 view; 11 | 12 | struct instance { 13 | float opacity; 14 | float exposure; 15 | mat4 transform; 16 | }; 17 | 18 | const int MAX_RENDER_COUNT = 100; 19 | 20 | layout(binding = 1, std140) uniform instance_data { //TODO store the single view matrix in here as well? 21 | int offsets[MAX_RENDER_COUNT]; //TODO pack the offsets into vec4s for improved size (increasing upload speed) 22 | instance instances[MAX_RENDER_COUNT]; 23 | }; 24 | 25 | void main() { 26 | instance i = instances[offsets[gl_DrawID] + gl_InstanceID]; 27 | gl_Position = view * i.transform * vec4(position, 0.0, 1.0); 28 | outTextureCoord = textureCoord; 29 | opacity = i.opacity; 30 | exposure = i.exposure; 31 | } -------------------------------------------------------------------------------- /client/resources/shaders/new/shading.comp: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0, binding = 0, r8ui) readonly uniform uimage2D solidMap; 4 | layout(location = 1, binding = 1, r8ui) writeonly uniform uimage2D shadingMap; 5 | layout(location = 2) uniform uvec2 bounds; 6 | 7 | const int SUNLIGHT_RADIUS_BLOCKS = 1; 8 | const int DIMENSION = SUNLIGHT_RADIUS_BLOCKS * 2 + 1; 9 | const float MAX_DISTANCE = length(vec2(SUNLIGHT_RADIUS_BLOCKS, SUNLIGHT_RADIUS_BLOCKS)); 10 | const float START_RATIO = 0.5f; 11 | 12 | void main() { 13 | ivec2 blockPos = ivec2(bounds.x + gl_WorkGroupID.x, bounds.y + gl_WorkGroupID.y); 14 | float exposure = 0; 15 | //TODO try with ONLY distance from nearest light block, or just weight all distances.?? 16 | for(int row = blockPos.y - SUNLIGHT_RADIUS_BLOCKS; row <= blockPos.y + SUNLIGHT_RADIUS_BLOCKS; row++) { 17 | for(int column = blockPos.x - SUNLIGHT_RADIUS_BLOCKS; column <= blockPos.x + SUNLIGHT_RADIUS_BLOCKS; column++) { 18 | //exposure += imageLoad(solidMap, ivec2(column, row)).r; 19 | //TODO implement inverse square law 20 | uint blockLight = imageLoad(solidMap, ivec2(column, row)).r; 21 | float ratio = 1.0 + START_RATIO - distance(vec2(column, row), vec2(blockPos))/MAX_DISTANCE; 22 | float current = (blockLight * ratio)/255.0f; 23 | exposure = max(exposure, current * current); 24 | } 25 | } 26 | imageStore(shadingMap, ivec2(blockPos), uvec4(uint(exposure * 255), 0, 0, 0)); 27 | 28 | //Iterate over circular group of points: 29 | //https://stackoverflow.com/questions/40779343/java-loop-through-all-pixels-in-a-2d-circle-with-center-x-y-and-radius 30 | // for (int i = y-r; i < y+r; i++) { 31 | // for (int j = x; (j-x)^2 + (i-y)^2 <= r^2; j--) { 32 | // //in the circle 33 | // } 34 | // for (int j = x+1; (j-x)*(j-x) + (i-y)*(i-y) <= r*r; j++) { 35 | // //in the circle 36 | // } 37 | // } 38 | 39 | //if imageLoad is outside boundary, will return 0 40 | //TODO invert lighting values, 0 should be fully lit 41 | //TODO instead of average, do a sum that weights by distance of light and clamps at fully lit 42 | //where one adjacent fully lit block is enough to outweigh all other blocks dark 43 | } 44 | -------------------------------------------------------------------------------- /client/resources/shaders/new/shading_apply.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(location = 0) in smooth vec2 worldCoord; 4 | 5 | layout(location = 0) out vec4 color; 6 | 7 | layout(location = 1, binding = 0) uniform usampler2D tileLighting; 8 | 9 | const bool SMOOTH_LIGHTING = true; 10 | 11 | float lighting(ivec2 position) { 12 | return texelFetch(tileLighting, position, 0).r/255.0f; 13 | } 14 | 15 | void main() { 16 | if(SMOOTH_LIGHTING) { 17 | //bilinear interpolation 18 | float ratioX = fract(worldCoord.x); 19 | float ratioY = fract(worldCoord.y); 20 | int left = int(floor(worldCoord.x)); 21 | int right = int(ceil(worldCoord.x)); 22 | int bottom = int(floor(worldCoord.y)); 23 | int top = int(ceil(worldCoord.y)); 24 | float lightBottom = mix(lighting(ivec2(left, bottom)), lighting(ivec2(right, bottom)), ratioX); 25 | float lightTop = mix(lighting(ivec2(left, top)), lighting(ivec2(right, top)), ratioX); 26 | color = vec4(0, 0, 0, 1.0 - mix(lightBottom, lightTop, ratioY)); 27 | } else { 28 | color = vec4(0, 0, 0, 1.0 - lighting(ivec2(int(worldCoord.x + 0.5), int(worldCoord.y + 0.5)))); 29 | } 30 | } -------------------------------------------------------------------------------- /client/resources/shaders/new/shading_apply.geom: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | layout(points) in; 4 | layout(triangle_strip, max_vertices = 4) out; 5 | 6 | layout(location = 0) out smooth vec2 worldCoord; 7 | 8 | layout(location = 0) uniform vec4 view; //left x, bottom y, right x, top y 9 | 10 | void main() { 11 | worldCoord = vec2(view[0], view[3]); 12 | gl_Position = vec4(-1, 1, 0, 1); 13 | EmitVertex(); 14 | 15 | worldCoord = vec2(view[0], view[1]); 16 | gl_Position = vec4(-1, -1, 0, 1); 17 | EmitVertex(); 18 | 19 | worldCoord = vec2(view[2], view[3]); 20 | gl_Position = vec4(1, 1, 0, 1); 21 | EmitVertex(); 22 | 23 | worldCoord = vec2(view[2], view[1]); 24 | gl_Position = vec4(1, -1, 0, 1); 25 | EmitVertex(); 26 | 27 | EndPrimitive(); 28 | } 29 | -------------------------------------------------------------------------------- /client/resources/shaders/old/model.frag: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | 3 | in vec2 passTextureCoord; 4 | out vec4 pixelColor; 5 | uniform sampler2D atlasTexture; 6 | uniform float opacity, exposure; 7 | 8 | void main() { 9 | vec4 color = texture(atlasTexture, passTextureCoord); 10 | pixelColor = vec4(color.rgb * exposure, color.a * opacity); 11 | } -------------------------------------------------------------------------------- /client/resources/shaders/old/model.vert: -------------------------------------------------------------------------------- 1 | #version 410 core 2 | 3 | in vec2 position, textureCoord; 4 | out vec2 passTextureCoord; 5 | uniform mat4 view, transform; 6 | 7 | void main() { 8 | gl_Position = view * transform * vec4(position, 0.0, 1.0); 9 | passTextureCoord = textureCoord; 10 | } -------------------------------------------------------------------------------- /client/src/module-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains all Sandbox2D client code, but does not export anything. The client can be 3 | * run by using main class ritzow.sandbox.client.StartClient. TWL's PNGDecoder library 4 | * is required as an automatic module "PNGDecoder". LWJGL modules 5 | * org.lwjgl.opengl/openal/glfw are also required. 6 | * @author Solomon Ritzow 7 | */ 8 | module ritzow.sandbox.client { 9 | requires ritzow.sandbox.shared; 10 | requires static java.desktop; 11 | requires java.logging; 12 | requires org.lwjgl; 13 | requires org.lwjgl.glfw; 14 | requires org.lwjgl.openal; 15 | requires org.lwjgl.opengl; 16 | requires org.json; 17 | 18 | exports ritzow.sandbox.client; 19 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/GameLoop.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.locks.LockSupport; 5 | import ritzow.sandbox.util.Utility; 6 | 7 | import static ritzow.sandbox.client.data.StandardClientOptions.*; 8 | 9 | class GameLoop { 10 | public interface GameContext { 11 | void run(long deltaNanos); 12 | } 13 | 14 | private static GameContext current; 15 | 16 | private static long lastUpdate, lastUpdateTime; 17 | 18 | public static void start(GameContext initial) { 19 | if(current != null) 20 | throw new IllegalStateException("GameLoop already started"); 21 | current = Objects.requireNonNull(initial); 22 | lastUpdate = System.nanoTime(); 23 | while(current != null) { 24 | long frameStart = System.nanoTime(); 25 | //long frameStartMS = System.currentTimeMillis(); 26 | long delta = frameStart - lastUpdate; 27 | current.run(delta); 28 | lastUpdate = Math.max(frameStart, lastUpdate); 29 | lastUpdateTime = delta; 30 | if(LIMIT_FPS) Utility.limitFramerate(frameStart, FRAME_TIME_LIMIT); 31 | //LockSupport.parkUntil(frameStartMS + FRAME_TIME_LIMIT/1_000_000); 32 | if(PRINT_FPS) Utility.printFramerate(frameStart); 33 | } 34 | } 35 | 36 | public static long getLastUpdateTime() { 37 | return lastUpdateTime; 38 | } 39 | 40 | public static long updateDeltaTime() { 41 | long lastTime = lastUpdate; 42 | return (lastUpdate = System.nanoTime()) - lastTime; 43 | } 44 | 45 | public static void setContext(Runnable context) { 46 | current = delta -> context.run(); 47 | } 48 | 49 | public static void setContext(GameContext context) { 50 | current = context; 51 | } 52 | 53 | public static void stop() { 54 | current = null; 55 | } 56 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/GameState.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client; 2 | 3 | import ritzow.sandbox.client.graphics.Display; 4 | import ritzow.sandbox.client.graphics.LightRenderProgram; 5 | import ritzow.sandbox.client.graphics.ModelRenderer; 6 | import ritzow.sandbox.client.ui.StandardGuiRenderer; 7 | 8 | class GameState { 9 | private GameState() {} 10 | 11 | private static ModelRenderer modelRenderer; 12 | private static Display display; 13 | private static long cursorPick, cursorMallet; 14 | private static MainMenuContext menuContext; 15 | private static StandardGuiRenderer guiRenderer; 16 | private static LightRenderProgram lightRenderer; 17 | 18 | static Display display() { 19 | return display; 20 | } 21 | 22 | static ModelRenderer modelRenderer() { 23 | return modelRenderer; 24 | } 25 | 26 | static void modelRenderer(ModelRenderer program) { 27 | GameState.modelRenderer = program; 28 | } 29 | 30 | static void setDisplay(Display display) { 31 | GameState.display = display; 32 | } 33 | 34 | static long cursorPick() { 35 | return cursorPick; 36 | } 37 | 38 | static void setCursorPick(long cursorPick) { 39 | GameState.cursorPick = cursorPick; 40 | } 41 | 42 | static long cursorMallet() { 43 | return cursorMallet; 44 | } 45 | 46 | static void setCursorMallet(long cursorMallet) { 47 | GameState.cursorMallet = cursorMallet; 48 | } 49 | 50 | static MainMenuContext menuContext() { 51 | return menuContext; 52 | } 53 | 54 | static void setMenuContext(MainMenuContext menuContext) { 55 | GameState.menuContext = menuContext; 56 | } 57 | 58 | static StandardGuiRenderer guiRenderer() { 59 | return guiRenderer; 60 | } 61 | 62 | static void setGuiRenderer(StandardGuiRenderer guiRenderer) { 63 | GameState.guiRenderer = guiRenderer; 64 | } 65 | 66 | static void setLightRenderer(LightRenderProgram lightRenderer) { 67 | GameState.lightRenderer = lightRenderer; 68 | } 69 | 70 | static LightRenderProgram lightRenderer() { 71 | return lightRenderer; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/StartClient.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client; 2 | 3 | import java.io.IOException; 4 | import org.lwjgl.glfw.GLFWErrorCallback; 5 | import ritzow.sandbox.client.audio.AudioSystem; 6 | import ritzow.sandbox.client.data.StandardClientOptions; 7 | import ritzow.sandbox.client.graphics.Display; 8 | import ritzow.sandbox.client.graphics.RenderManager; 9 | import ritzow.sandbox.client.util.ClientUtility; 10 | import ritzow.sandbox.util.Utility; 11 | 12 | import static org.lwjgl.glfw.GLFW.*; 13 | import static ritzow.sandbox.client.data.StandardClientProperties.CURSORS_PATH; 14 | import static ritzow.sandbox.client.data.StandardClientProperties.TEXTURES_PATH; 15 | import static ritzow.sandbox.client.util.ClientUtility.log; 16 | 17 | /** 18 | * Entry point to Sandbox2D game client 19 | **/ 20 | public class StartClient { 21 | 22 | /** 23 | * Command line entry point. 24 | * @param args command line arguments. 25 | * @throws IOException if the program encounters an error. 26 | **/ 27 | public static void main(String... args) throws IOException { 28 | log().info("Starting game"); 29 | long startupStart = System.nanoTime(); 30 | AudioSystem.getDefault(); //load default audio system 31 | setupGLFW(); 32 | RenderManager.setup(); 33 | GameState.setLightRenderer(RenderManager.LIGHT_RENDERER); 34 | GameState.modelRenderer(RenderManager.MODEL_RENDERER); 35 | MainMenuContext mainMenu = new MainMenuContext(); 36 | GameState.setMenuContext(mainMenu); 37 | log().info("Game startup took " + Utility.formatTime(Utility.nanosSince(startupStart))); 38 | GameState.display().show(); 39 | GameLoop.start(mainMenu::update); 40 | } 41 | 42 | public static void shutdown() { 43 | log().info("Exiting game"); 44 | GameState.modelRenderer().delete(); 45 | RenderManager.closeContext(); 46 | GameState.display().destroy(); 47 | AudioSystem.getDefault().close(); 48 | glfwDestroyCursor(GameState.cursorPick()); 49 | glfwDestroyCursor(GameState.cursorMallet()); 50 | glfwTerminate(); 51 | log().info("Game exited"); 52 | GameLoop.stop(); 53 | } 54 | 55 | private static void setupGLFW() throws IOException { 56 | log().info("Loading GLFW and creating window"); 57 | glfwSetErrorCallback(GLFWErrorCallback.createThrow()); 58 | if(!glfwInit()) 59 | throw new RuntimeException("GLFW failed to initialize"); 60 | var appIcon = ClientUtility.loadGLFWImage(TEXTURES_PATH.resolve("redSquare.png")); 61 | var cursorPickaxe = ClientUtility.loadGLFWImage(CURSORS_PATH.resolve("pickaxe32.png")); 62 | var cursorMallet = ClientUtility.loadGLFWImage(CURSORS_PATH.resolve("mallet32.png")); 63 | GameState.setCursorPick(ClientUtility.loadGLFWCursor(cursorPickaxe, 0, 0.66f)); 64 | GameState.setCursorMallet(ClientUtility.loadGLFWCursor(cursorMallet, 0, 0.66f)); 65 | GameState.setDisplay(new Display(4, StandardClientOptions.USE_OPENGL_4_6 ? 6 : 3, "Sandbox2D", appIcon)); 66 | RenderManager.OPENGL_CAPS = GameState.display().setGraphicsContextOnThread(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/audio/AudioData.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.audio; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public interface AudioData { 6 | short getBitsPerSample(); 7 | boolean isSigned(); 8 | short getChannels(); 9 | int getSampleRate(); 10 | ByteBuffer getData(); 11 | } 12 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/audio/AudioSystem.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.audio; 2 | 3 | import java.io.IOException; 4 | import ritzow.sandbox.client.audio.Sound.StandardSound; 5 | 6 | import static ritzow.sandbox.client.data.StandardClientProperties.AUDIO_PATH; 7 | import static ritzow.sandbox.client.util.ClientUtility.log; 8 | 9 | //TODO audio system should know about camera location and other things 10 | public interface AudioSystem { 11 | 12 | default void playSound(Sound sound, float x, float y, float velocityX, float velocityY) { 13 | playSound(sound, x, y, velocityX, velocityY, 1.0f, 1.0f); 14 | } 15 | 16 | void playSound(Sound sound, float x, float y, float velocityX, float velocityY, float gain, float pitch); 17 | void playSoundGlobal(Sound sound, float gain, float pitch); 18 | void registerSound(Sound sound, AudioData data); 19 | void setVolume(float gain); 20 | void setPosition(float x, float y); 21 | void close(); 22 | 23 | static AudioSystem getDefault() { 24 | return Default.DEFAULT; 25 | } 26 | 27 | final class Default { 28 | private Default() {} 29 | private static final AudioSystem DEFAULT = loadDefault(); 30 | private static AudioSystem loadDefault() { 31 | try { 32 | log().info("Loading audio system"); 33 | OpenALAudioSystem.initOpenAL(); 34 | AudioSystem audio = OpenALAudioSystem.create(); 35 | audio.setVolume(1.0f); 36 | for(var sound : StandardSound.values()) { 37 | audio.registerSound(sound, 38 | WAVEDecoder.decode(AUDIO_PATH.resolve(sound.fileName()))); 39 | } 40 | return audio; 41 | } catch (IOException e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/audio/JavaxAudioSystem.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.audio; 2 | 3 | import java.io.IOException; 4 | import java.util.Collection; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.ConcurrentLinkedQueue; 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | import javax.sound.sampled.AudioFormat; 11 | import javax.sound.sampled.AudioSystem; 12 | import javax.sound.sampled.DataLine; 13 | import javax.sound.sampled.FloatControl; 14 | import javax.sound.sampled.FloatControl.Type; 15 | import javax.sound.sampled.LineUnavailableException; 16 | import javax.sound.sampled.Mixer; 17 | import javax.sound.sampled.SourceDataLine; 18 | 19 | public final class JavaxAudioSystem implements ritzow.sandbox.client.audio.AudioSystem { 20 | private final Collection lines; 21 | private final DataLine.Info info; 22 | private final Mixer mixer; 23 | private final Map sounds; 24 | 25 | public static JavaxAudioSystem create(AudioData init) throws IOException { 26 | return new JavaxAudioSystem(init); 27 | } 28 | 29 | public JavaxAudioSystem(AudioData init) throws IOException { 30 | try { 31 | mixer = AudioSystem.getMixer(null); //get default mixer 32 | mixer.open(); 33 | info = new DataLine.Info(SourceDataLine.class, getFormat(init)); 34 | lines = new ConcurrentLinkedQueue<>(); 35 | sounds = new ConcurrentHashMap<>(); 36 | for(int i = 0; i < 5; i++) { 37 | createLine(); 38 | } 39 | } catch (LineUnavailableException e) { 40 | throw new IOException(e); 41 | } 42 | } 43 | 44 | @Override 45 | public void registerSound(Sound id, AudioData data) { 46 | if(sounds.containsKey(id)) 47 | throw new IllegalStateException(id + " already registered"); 48 | sounds.put(id, getData(data)); 49 | } 50 | 51 | private static byte[] getData(AudioData info) { 52 | byte[] data = new byte[info.getData().capacity()]; 53 | info.getData().get(data); 54 | return data; 55 | } 56 | 57 | private static AudioFormat getFormat(AudioData info) { 58 | return new AudioFormat( 59 | info.getSampleRate(), 60 | info.getBitsPerSample(), 61 | info.getChannels(), 62 | info.isSigned(), 63 | false 64 | ); 65 | } 66 | 67 | private final ExecutorService worker = Executors.newCachedThreadPool(); 68 | 69 | @Override 70 | public void playSound(Sound sound, float x, float y, float velocityX, float velocityY, float gain, float pitch) { 71 | worker.execute(()-> { 72 | byte[] data = sounds.get(sound); 73 | if(data == null) 74 | throw new IllegalArgumentException("no such sound"); 75 | selectLine().write(data, 0, data.length); 76 | }); 77 | } 78 | 79 | private SourceDataLine selectLine() { 80 | synchronized(lines) { 81 | for(var line : lines) { 82 | if(!line.isActive()) { 83 | return line; 84 | } 85 | } 86 | } 87 | 88 | if(!reachedMaxLines()) { 89 | try { 90 | return createLine(); 91 | } catch (LineUnavailableException e) { 92 | throw new RuntimeException(e); 93 | } 94 | } 95 | return null; 96 | } 97 | 98 | private boolean reachedMaxLines() { 99 | int max = mixer.getMaxLines(info); 100 | return max != -1 && max > lines.size(); 101 | } 102 | 103 | private SourceDataLine createLine() throws LineUnavailableException { 104 | var line = (SourceDataLine)mixer.getLine(info); 105 | line.open(); line.start(); 106 | lines.add(line); 107 | return line; 108 | } 109 | 110 | @Override 111 | public void playSoundGlobal(Sound sound, float gain, float pitch) { 112 | throw new UnsupportedOperationException("not implemented"); 113 | } 114 | 115 | @Override 116 | public void setVolume(float gain) { 117 | synchronized(lines) { 118 | for(var line : lines) { 119 | ((FloatControl)line.getControl(Type.MASTER_GAIN)).setValue(gain); 120 | } 121 | } 122 | } 123 | 124 | @Override 125 | public void setPosition(float x, float y) { 126 | 127 | } 128 | 129 | @Override 130 | public void close() { 131 | for(var line : lines) { 132 | line.close(); 133 | } 134 | mixer.close(); 135 | worker.shutdown(); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/audio/OpenALException.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.audio; 2 | 3 | public class OpenALException extends RuntimeException { 4 | 5 | public OpenALException() { 6 | } 7 | 8 | public OpenALException(String message) { 9 | super(message); 10 | } 11 | 12 | public OpenALException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public OpenALException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public OpenALException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/audio/Sound.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.audio; 2 | 3 | import java.nio.file.Path; 4 | 5 | public interface Sound { 6 | 7 | enum StandardSound implements Sound { 8 | BLOCK_BREAK(Path.of("dig.wav")), 9 | BLOCK_PLACE(Path.of("place.wav")), 10 | POP(Path.of("pop.wav")), 11 | SNAP(Path.of("snap.wav")), 12 | THROW(Path.of("throw.wav")); 13 | 14 | private final Path fileName; 15 | 16 | StandardSound(Path fileName) { 17 | this.fileName = fileName; 18 | } 19 | 20 | public Path fileName() { 21 | return fileName; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/build/CompileShaders.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.build; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.ArrayDeque; 7 | import java.util.Queue; 8 | 9 | public class CompileShaders { 10 | private static final Path DEST = Path.of("resources/shaders/new/out"); 11 | 12 | private static final boolean DEBUG = true; 13 | 14 | private static record CompileTask(Process process, String name) {} 15 | 16 | public static void main(String... args) throws IOException, InterruptedException { 17 | Queue processes = new ArrayDeque<>(); 18 | for(Path file : Files.newDirectoryStream(Path.of("resources/shaders/new"), Files::isRegularFile)) { 19 | Path destFile = DEST.resolve(file.getFileName() + ".spv"); 20 | if(Files.notExists(destFile) || Files.getLastModifiedTime(file).compareTo(Files.getLastModifiedTime(destFile)) > 0) { 21 | processes.add(build(file, destFile)); 22 | } 23 | } 24 | while(!processes.isEmpty()) { 25 | CompileTask next = processes.poll(); 26 | System.out.printf("%30s compilation exited with value %d%n", next.name, next.process.waitFor()); 27 | } 28 | } 29 | 30 | private static CompileTask build(Path file, Path dest) throws IOException { 31 | //https://www.khronos.org/opengles/sdk/tools/Reference-Compiler/ 32 | //TODO look into -l option "link all input files together to form a single module" to validate glsl programs and create program files 33 | ProcessBuilder builder = DEBUG ? new ProcessBuilder( 34 | "glslangValidator", 35 | "--target-env", "opengl", 36 | "--client", "opengl100", 37 | "-Od", 38 | "-t", 39 | //"-H", //print opcodes 40 | "-g", //print debug info 41 | "-e", "main", 42 | "-o", dest.toString(), 43 | file.toString() 44 | ) : new ProcessBuilder( 45 | "glslangValidator", 46 | "--target-env", "opengl", 47 | "--client", "opengl100", 48 | "--quiet", 49 | "-Os", 50 | "-t", 51 | DEBUG ? "-g" : "-g0", //strip debug info 52 | "-e", "main", 53 | "-o", dest.toString(), 54 | file.toString() 55 | ); 56 | 57 | return new CompileTask(builder.inheritIO().start(), file.getFileName().toString()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/data/ClientOptions.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.data; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | import java.nio.file.Files; 6 | import java.util.Map; 7 | import java.util.function.Function; 8 | import java.util.stream.Collectors; 9 | 10 | class ClientOptions { 11 | private static final Map OPTIONS = loadOptions(); 12 | 13 | // private static Map loadOptions() { 14 | // try(var in = Files.newInputStream(StandardClientProperties.OPTIONS_PATH)) { 15 | // Properties options = new Properties(); 16 | // options.load(in); 17 | // return (Map)options; 18 | // } catch(IOException e) { 19 | // e.printStackTrace(); 20 | // return Map.of(); 21 | // } 22 | // } 23 | 24 | private static Map loadOptions() { 25 | try { 26 | return Files.lines(StandardClientProperties.OPTIONS_PATH, StandardCharsets.UTF_8) 27 | .filter(line -> !line.isBlank() && !line.stripLeading().startsWith("#")) 28 | .collect(Collectors.toMap( 29 | line -> line.substring(0, line.indexOf('=')).strip(), 30 | line -> line.substring(line.indexOf('=') + 1).strip() 31 | )); 32 | } catch(IOException e) { 33 | e.printStackTrace(); 34 | return Map.of(); 35 | } 36 | } 37 | 38 | static String get(String option, String defaultValue) { 39 | return OPTIONS.getOrDefault(option, defaultValue); 40 | } 41 | 42 | static T get(String option, T defaultValue, Function converter) { 43 | var val = OPTIONS.getOrDefault(option, ""); 44 | return val.isEmpty() ? defaultValue : converter.apply(val); 45 | } 46 | 47 | private ClientOptions() {} 48 | } 49 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/data/StandardClientOptions.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.data; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.net.SocketException; 5 | import java.net.UnknownHostException; 6 | import ritzow.sandbox.network.NetworkUtility; 7 | import ritzow.sandbox.network.Protocol; 8 | import ritzow.sandbox.util.Utility; 9 | 10 | import static ritzow.sandbox.client.data.ClientOptions.get; 11 | import static ritzow.sandbox.network.NetworkUtility.parseSocket; 12 | 13 | public class StandardClientOptions { 14 | 15 | //Rendering 16 | public static final boolean USE_OPENGL_4_6 = get("use_new_opengl", false, Boolean::parseBoolean); 17 | public static final boolean PRINT_FPS = get("fps_print", false, Boolean::parseBoolean); 18 | public static final long FRAME_TIME_LIMIT = 19 | get("fps_limit", 0L, StandardClientOptions::frameTimeLimit); //TODO doesn't work for very large framerates because there is not enough granularity 20 | public static final boolean LIMIT_FPS = FRAME_TIME_LIMIT > 0; 21 | public static final boolean VSYNC = get("vsync", false, Boolean::parseBoolean); //TODO make LIMIT_FPS a combo with this as a string enum 22 | public static final boolean USE_INTERNET = get("use_internet", true, Boolean::parseBoolean); 23 | public static final float GUI_SCALE = get("gui_scale", 500f, Float::parseFloat); 24 | 25 | public static final boolean DEBUG = get("debug", false, Boolean::parseBoolean); 26 | public static final boolean DEBUG_OPENGL = get("debug_opengl", false, Boolean::parseBoolean); 27 | public static final boolean DISABLE_CLIENT_UPDATE = get("disable_client_update", false, Boolean::parseBoolean); 28 | public static final boolean LEFTY = get("lefty", false, Boolean::parseBoolean); 29 | 30 | private static long frameTimeLimit(String value) { 31 | return Utility.frameRateToFrameTimeNanos(Long.parseUnsignedLong(value)); 32 | } 33 | 34 | //Input 35 | public static final double SELECT_SENSITIVITY = get("scroll_sensitivity", 1.0, Double::parseDouble); 36 | 37 | //Network 38 | private static InetSocketAddress LOCAL_ADDRESS, SERVER_ADDRESS; 39 | 40 | public static InetSocketAddress getServerAddress() { 41 | return SERVER_ADDRESS == null ? SERVER_ADDRESS = 42 | get("address_server", defaultAddress(Protocol.DEFAULT_SERVER_PORT), 43 | string -> parseSocket(string, Protocol.DEFAULT_SERVER_PORT)) : SERVER_ADDRESS; 44 | } 45 | 46 | public static InetSocketAddress getLocalAddress() { 47 | return LOCAL_ADDRESS == null ? LOCAL_ADDRESS = 48 | get("address_local", defaultAddress(NetworkUtility.ANY_PORT), 49 | string -> parseSocket(string, 0)) : LOCAL_ADDRESS; 50 | } 51 | 52 | private static InetSocketAddress defaultAddress(int port) { 53 | try { 54 | return USE_INTERNET ? NetworkUtility.getPublicSocket(port) : 55 | new InetSocketAddress(NetworkUtility.getLoopbackAddress(), port); 56 | } catch(SocketException | UnknownHostException e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/data/StandardClientProperties.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.data; 2 | 3 | import java.nio.file.Path; 4 | 5 | public class StandardClientProperties { 6 | public static final Path WORKING_DIR = Path.of("."); 7 | public static final Path OPTIONS_PATH = WORKING_DIR.resolve("options.txt"); 8 | public static final Path RESOURCES_PATH = WORKING_DIR.resolve("resources"); 9 | public static final Path ASSETS_PATH = RESOURCES_PATH.resolve("assets"); 10 | public static final Path TEXTURES_PATH = ASSETS_PATH.resolve("textures"); 11 | public static final Path CURSORS_PATH = TEXTURES_PATH.resolve("cursors"); 12 | public static final Path AUDIO_PATH = ASSETS_PATH.resolve("audio"); 13 | public static final Path SHADERS_PATH = RESOURCES_PATH.resolve("shaders"); 14 | } 15 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Camera.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public class Camera { 4 | protected float positionX; 5 | protected float positionY; 6 | protected float zoom; 7 | 8 | public Camera(float positionX, float positionY, float zoom) { 9 | this.positionX = positionX; 10 | this.positionY = positionY; 11 | this.zoom = zoom; 12 | } 13 | 14 | public final float getPositionX() { 15 | return positionX; 16 | } 17 | 18 | public final float getPositionY() { 19 | return positionY; 20 | } 21 | 22 | public final float getZoom() { 23 | return zoom; 24 | } 25 | 26 | public final void setPositionX(float positionX) { 27 | this.positionX = positionX; 28 | } 29 | 30 | public final void setPositionY(float positionY) { 31 | this.positionY = positionY; 32 | } 33 | 34 | public final void setZoom(float zoom) { 35 | this.zoom = zoom; 36 | } 37 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Framebuffer.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import static org.lwjgl.opengl.GL30C.*; 4 | 5 | public final class Framebuffer { 6 | public final int framebufferID; 7 | 8 | public Framebuffer(int id) { 9 | this.framebufferID = id; 10 | } 11 | 12 | public Framebuffer() { 13 | this.framebufferID = glGenFramebuffers(); 14 | GraphicsUtility.checkErrors(); 15 | } 16 | 17 | public int id() { 18 | return framebufferID; 19 | } 20 | 21 | public void setCurrent() { 22 | glBindFramebuffer(GL_FRAMEBUFFER, framebufferID); 23 | } 24 | 25 | public void copyTo(Framebuffer destination, int x, int y, int width, int height) { 26 | setRead(); 27 | destination.setDraw(); 28 | glBlitFramebuffer(x, y, width, height, x, y, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR); 29 | } 30 | 31 | public void clear(float red, float green, float blue, float alpha) { 32 | setDraw(); 33 | //glBindFramebuffer(GL_FRAMEBUFFER, framebufferID); 34 | glClearColor(red, green, blue, alpha); 35 | glClear(GL_COLOR_BUFFER_BIT); 36 | } 37 | 38 | public void attachTextureColor(int texture) { 39 | attachTexture(texture, GL_COLOR_ATTACHMENT0); 40 | } 41 | 42 | public void attachTexture(OpenGLTexture texture, int attachment) { 43 | attachTexture(texture.id, attachment); 44 | } 45 | 46 | public void attachTexture(int texture, int attachment) { 47 | glBindFramebuffer(GL_FRAMEBUFFER, framebufferID); 48 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + attachment, GL_TEXTURE_2D, texture, 0); 49 | } 50 | 51 | public void setRead() { 52 | glBindFramebuffer(GL_READ_FRAMEBUFFER, framebufferID); 53 | } 54 | 55 | public void setDraw() { 56 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebufferID); 57 | } 58 | 59 | public void delete() { 60 | glDeleteFramebuffers(framebufferID); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/FullscreenQuadProgram.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import static org.lwjgl.opengl.GL46C.*; 4 | 5 | public class FullscreenQuadProgram extends ShaderProgram { 6 | private final int vao; 7 | 8 | public FullscreenQuadProgram(Shader vertex, Shader geometry, Shader fragment) { 9 | super(vertex, geometry, fragment); 10 | vao = glGenVertexArrays(); 11 | } 12 | 13 | public void render(int sourceTexture, Framebuffer dest) { 14 | setCurrent(); 15 | glActiveTexture(GL_TEXTURE0); 16 | glBindTexture(GL_TEXTURE_2D, sourceTexture); 17 | 18 | glBindVertexArray(vao); 19 | glDrawArrays(GL_POINTS, 0, 1); 20 | } 21 | 22 | @Override 23 | public void delete() { 24 | super.delete(); 25 | glDeleteVertexArrays(vao); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/GameModels.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public class GameModels { 4 | public static Model 5 | MODEL_GRASS_BLOCK, 6 | MODEL_DIRT_BLOCK, 7 | MODEL_GLASS_BLOCK, 8 | MODEL_GREEN_FACE, 9 | MODEL_RED_SQUARE, 10 | MODEL_SKY, 11 | MODEL_BLUE_SQUARE, 12 | MODEL_NIGHT_SKY, 13 | MODEL_ATLAS, 14 | STICKMAN_STAND, 15 | STICKMAN_CROUCH; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Graphics.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | /** 4 | * A Graphics instance represents a model ID, opacity, scale, and rotation 5 | * @author Solomon Ritzow 6 | * 7 | */ 8 | public interface Graphics { 9 | Model getModel(); 10 | float getOpacity(); 11 | float getScaleX(); 12 | float getScaleY(); 13 | float getRotation(); 14 | } 15 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/GraphicsUtility.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.Map; 5 | 6 | import static org.lwjgl.opengl.GL46C.*; 7 | 8 | public final class GraphicsUtility { 9 | 10 | private GraphicsUtility() {} 11 | 12 | private static final Map errorMessages = Map.ofEntries( 13 | Map.entry(0x500, "Invalid enum"), 14 | Map.entry(0x501, "Invalid value"), 15 | Map.entry(0x502, "Invalid operation"), 16 | Map.entry(0x506, "Attempt to use incomplete framebuffer"), 17 | Map.entry(0x507, "OpenGL context lost") 18 | ); 19 | 20 | /** Creates an fixed-size texture with the provided data **/ 21 | public static int uploadTextureData(ByteBuffer pixels, int width, int height) { 22 | int id = glGenTextures(); 23 | glBindTexture(GL_TEXTURE_2D, id); 24 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 25 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 26 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 27 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 28 | glTexStorage2D(GL_TEXTURE_2D, 1, GL_SRGB8_ALPHA8, width, height); 29 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); 30 | glBindTexture(GL_TEXTURE_2D, 0); 31 | GraphicsUtility.checkErrors(); 32 | return id; 33 | } 34 | 35 | public static void checkErrors() throws OpenGLException { 36 | int error = org.lwjgl.opengl.GL11.glGetError(); 37 | if(error != 0) { 38 | throw new OpenGLException("Error Code: " + error + " (" + errorMessages.get(error) + ")"); 39 | } 40 | } 41 | 42 | public static void checkFramebufferCompleteness(int framebuffer) { 43 | glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); 44 | int status = glCheckFramebufferStatus(GL_FRAMEBUFFER); 45 | if (status != GL_FRAMEBUFFER_COMPLETE) { 46 | throw new OpenGLException("OpenGL Framebuffer Error: " + status); 47 | } 48 | } 49 | 50 | public static void checkFramebufferCompleteness(Framebuffer framebuffer) { 51 | glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.framebufferID); 52 | int status = glCheckFramebufferStatus(GL_FRAMEBUFFER); 53 | if (status != GL_FRAMEBUFFER_COMPLETE) { 54 | throw new OpenGLException("Framebuffer incomplete: " + status); 55 | } 56 | } 57 | 58 | public static void checkShaderCompilation(Shader...shaders) { 59 | for(Shader s : shaders) { 60 | checkShaderCompilation(s); 61 | } 62 | } 63 | 64 | public static void checkShaderCompilation(Shader shader) { 65 | if(glGetShaderi(shader.getShaderID(), GL_COMPILE_STATUS) != 1) { 66 | String error = glGetShaderInfoLog(shader.getShaderID()); 67 | glDeleteShader(shader.getShaderID()); 68 | throw new OpenGLException("Shader Compilation Error" + (error.length() > 0 ? ": " + error : "")); 69 | } 70 | } 71 | 72 | public static void checkProgramCompilation(ShaderProgram program) { 73 | if(glGetProgrami(program.programID, GL_LINK_STATUS) != 1) { 74 | throw new OpenGLException("Shader program failed linking: " + glGetProgramInfoLog(program.programID)); 75 | } 76 | } 77 | 78 | /** 79 | * glValidateProgram() is meant to be called directly before a draw call 80 | * with that shader bound and all the bindings (VAO, textures) set. Its 81 | * purpose is to ensure that the shader can execute given the current GL 82 | * state. So, you should not be calling it as part of your shader initialization. 83 | * @param program the program to validate 84 | */ 85 | public static void checkProgramValidation(ShaderProgram program) { 86 | if(glGetProgrami(program.programID, GL_VALIDATE_STATUS) != 1) { 87 | throw new OpenGLException("Shader program failed validation:\n" + glGetProgramInfoLog(program.programID)); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Light.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public abstract class Light { 4 | public abstract float posX(); 5 | public abstract float posY(); 6 | public abstract float red(); 7 | public abstract float green(); 8 | public abstract float blue(); 9 | public abstract float intensity(); 10 | } 11 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/LightRenderProgram.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.ByteBuffer; 4 | import org.lwjgl.BufferUtils; 5 | import ritzow.sandbox.client.util.ClientUtility; 6 | 7 | import static org.lwjgl.opengl.GL46C.*; 8 | 9 | /** 10 | * Point lighting system: 11 | * 12 | * 1. The view information is set so that calls to queueLight can compute the proper stuff 13 | * 2. glDrawArrays with the proper light count is called 14 | * 3. Geometry shader produces a square for each light at the proper position in the screen space 15 | * 4. Shader interpolates relative position within light and sends to fragment shader 16 | * 5. Fragment shader computes all pixel colors within light square using the interpolated values 17 | */ 18 | public class LightRenderProgram extends ShaderProgram { 19 | private static final int MAX_LIGHTS = 100; 20 | private static final int STRIDE_BYTES = 2 * Float.BYTES + 3 * Float.BYTES + 1 * Float.BYTES; //pos, color, intensity 21 | private static final int OFFSET_POSITION = 0, OFFSET_COLOR = OFFSET_POSITION + 2 * Float.BYTES, OFFSET_INTENSITY = OFFSET_COLOR + 3 * Float.BYTES; 22 | 23 | private final ByteBuffer lightsBuffer; 24 | private final int vao, vbo, uniformView, uniformScale; 25 | private int count; 26 | 27 | public LightRenderProgram(Shader vertex, Shader geometry, Shader fragment) { 28 | super(vertex, geometry, fragment); 29 | vao = glGenVertexArrays(); 30 | glBindVertexArray(vao); 31 | 32 | int lightsVBO = glGenBuffers(); 33 | vbo = lightsVBO; 34 | glBindBuffer(GL_ARRAY_BUFFER, lightsVBO); 35 | int bufferSize = STRIDE_BYTES * MAX_LIGHTS; 36 | glBufferStorage(GL_ARRAY_BUFFER, bufferSize, GL_DYNAMIC_STORAGE_BIT); 37 | lightsBuffer = BufferUtils.createByteBuffer(bufferSize); 38 | 39 | //position 40 | int positionAttribute = getAttributeLocation("position"); 41 | glEnableVertexAttribArray(positionAttribute); 42 | glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, false, STRIDE_BYTES, OFFSET_POSITION); 43 | 44 | //color 45 | int colorAttribute = getAttributeLocation("color"); 46 | glEnableVertexAttribArray(colorAttribute); 47 | glVertexAttribPointer(colorAttribute, 3, GL_FLOAT, false, STRIDE_BYTES, OFFSET_COLOR); 48 | 49 | //intensity 50 | int intensityAttribute = getAttributeLocation("intensity"); 51 | glEnableVertexAttribArray(intensityAttribute); 52 | glVertexAttribPointer(intensityAttribute, 1, GL_FLOAT, false, STRIDE_BYTES, OFFSET_INTENSITY); 53 | 54 | this.uniformView = getUniformLocation("view"); 55 | this.uniformScale = getUniformLocation("scale"); 56 | 57 | GraphicsUtility.checkErrors(); 58 | } 59 | 60 | //private Camera camera; 61 | //private int frameWidth, frameHeight; 62 | 63 | private final float[] viewMatrix = { 64 | 0, 0, 0, 0, 65 | 0, 0, 0, 0, 66 | 0, 0, 0, 0, 67 | 0, 0, 0, 1, 68 | }; 69 | 70 | public void setup(Camera camera, OpenGLTexture diffuse, int frameWidth, int frameHeight) { 71 | //this.camera = camera; 72 | //this.frameWidth = frameWidth; 73 | //this.frameHeight = frameHeight; 74 | setCurrent(); 75 | 76 | //set solid texture 77 | glActiveTexture(GL_TEXTURE0); 78 | diffuse.bind(); 79 | 80 | ClientUtility.setViewMatrix(viewMatrix, camera, frameWidth, frameHeight); 81 | setMatrix(uniformView, viewMatrix); 82 | setFloat(uniformScale, camera.getZoom()); 83 | glBlendFunc(GL_SRC_ALPHA, GL_ONE); 84 | glBlendEquation(GL_FUNC_ADD); 85 | glBindVertexArray(vao); 86 | glBindBuffer(GL_ARRAY_BUFFER, vbo); 87 | } 88 | 89 | public void queueLight(Light light, float offsetX, float offsetY) { 90 | lightsBuffer 91 | .putFloat(light.posX() + offsetX) 92 | .putFloat(light.posY() + offsetY) 93 | .putFloat(light.red()) 94 | .putFloat(light.green()) 95 | .putFloat(light.blue()) 96 | .putFloat(light.intensity()); 97 | count++; 98 | if(count == MAX_LIGHTS) { 99 | flush(); 100 | } 101 | } 102 | 103 | public void flush() { 104 | glBufferSubData(GL_ARRAY_BUFFER, 0, lightsBuffer.flip()); 105 | glDrawArrays(GL_POINTS, 0, count); 106 | count = 0; 107 | lightsBuffer.clear(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/LightingApplyProgram.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import static org.lwjgl.opengl.GL46C.*; 4 | 5 | public class LightingApplyProgram extends ShaderProgram { 6 | private final int vao, diffuse, lighting; 7 | 8 | public LightingApplyProgram(Shader vertex, Shader geometry, Shader fragment) { 9 | super(vertex, geometry, fragment); 10 | vao = glGenVertexArrays(); 11 | diffuse = getUniformLocation("diffuse"); 12 | lighting = getUniformLocation("lighting"); 13 | setInteger(diffuse, 0); 14 | setInteger(lighting, 1); 15 | } 16 | 17 | public void render(OpenGLTexture diffuse, OpenGLTexture lighting) { 18 | setCurrent(); 19 | glActiveTexture(GL_TEXTURE0); 20 | glBindTexture(GL_TEXTURE_2D, diffuse.id); 21 | 22 | glActiveTexture(GL_TEXTURE1); 23 | glBindTexture(GL_TEXTURE_2D, lighting.id); 24 | 25 | glBindVertexArray(vao); 26 | glDrawArrays(GL_POINTS, 0, 1); 27 | } 28 | 29 | @Override 30 | public void delete() { 31 | super.delete(); 32 | glDeleteVertexArrays(vao); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Lit.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import ritzow.sandbox.world.component.Positional; 4 | 5 | public interface Lit extends Positional { 6 | Iterable lights(); 7 | } 8 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Model.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public /*sealed*/ interface Model /*permits ModelRenderProgramBase.ModelAttributes*/ { 4 | float width(); 5 | float height(); 6 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ModelDestination.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public interface ModelDestination { 4 | void set(Model model); 5 | } 6 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ModelRenderProgramBase.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import ritzow.sandbox.client.graphics.ModelStorage.ModelBuffers; 4 | import ritzow.sandbox.client.util.ClientUtility; 5 | 6 | import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D; 7 | import static org.lwjgl.opengl.GL11C.glBindTexture; 8 | import static org.lwjgl.opengl.GL13C.glActiveTexture; 9 | import static org.lwjgl.opengl.GL15C.GL_TEXTURE0; 10 | 11 | public abstract class ModelRenderProgramBase extends ShaderProgram implements ModelRenderer { 12 | private static final int ATLAS_TEXTURE_UNIT = GL_TEXTURE0; 13 | 14 | protected final float[] aspectMatrix = { 15 | 1, 0, 0, 0, 16 | 0, 1, 0, 0, 17 | 0, 0, 0, 0, 18 | 0, 0, 0, 1, 19 | }; 20 | 21 | protected final float[] viewMatrix = { 22 | 0, 0, 0, 0, 23 | 0, 0, 0, 0, 24 | 0, 0, 0, 0, 25 | 0, 0, 0, 1, 26 | }; 27 | 28 | protected final int uniform_view, vaoID, atlasTexture; 29 | 30 | public ModelRenderProgramBase(Shader vertex, Shader fragment, int textureAtlas, ModelBuffers models) { 31 | super(vertex, fragment); 32 | this.uniform_view = getUniformLocation("view"); 33 | this.atlasTexture = textureAtlas; 34 | this.vaoID = ModelStorage.defineVAO(models, getAttributeLocation("position"), getAttributeLocation("textureCoord")); 35 | setInteger(getUniformLocation("atlasSampler"), ATLAS_TEXTURE_UNIT); 36 | } 37 | 38 | @Override 39 | public void prepare() { 40 | setCurrent(); 41 | glActiveTexture(ATLAS_TEXTURE_UNIT); 42 | glBindTexture(GL_TEXTURE_2D, atlasTexture); 43 | //TODO maybe move this to client code for more control ie when rendering lights 44 | //set the blending mode to allow transparency 45 | } 46 | 47 | @Override 48 | public void loadViewMatrixIdentity() { 49 | flush(); 50 | aspectMatrix[0] = 1; 51 | aspectMatrix[5] = 1; 52 | setMatrix(uniform_view, aspectMatrix); 53 | } 54 | 55 | @Override 56 | public void loadViewMatrixScale(float guiScale, int framebufferWidth, int framebufferHeight) { 57 | flush(); 58 | GraphicsUtility.checkErrors(); 59 | aspectMatrix[0] = guiScale/framebufferWidth; 60 | aspectMatrix[5] = guiScale/framebufferHeight; 61 | setMatrix(uniform_view, aspectMatrix); 62 | GraphicsUtility.checkErrors(); 63 | } 64 | 65 | @Override 66 | public void loadViewMatrixStandard(int framebufferWidth, int framebufferHeight) { 67 | flush(); 68 | aspectMatrix[0] = framebufferHeight/(float)framebufferWidth; 69 | aspectMatrix[5] = 1; 70 | setMatrix(uniform_view, aspectMatrix); 71 | } 72 | 73 | @Override 74 | public void loadViewMatrix(Camera camera, int framebufferWidth, int framebufferHeight) { 75 | flush(); 76 | //TODO add camera rotation 77 | ClientUtility.setViewMatrix(viewMatrix, camera, framebufferWidth, framebufferHeight); 78 | setMatrix(uniform_view, viewMatrix); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ModelRenderProgramOld.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.FloatBuffer; 4 | import org.lwjgl.BufferUtils; 5 | import ritzow.sandbox.client.graphics.ModelStorage.ModelBuffers; 6 | import ritzow.sandbox.client.graphics.ModelStorage.ModelAttributes; 7 | 8 | import static org.lwjgl.opengl.GL46C.*; 9 | 10 | public class ModelRenderProgramOld extends ModelRenderProgramBase { 11 | private final int uniform_transform, uniform_opacity, uniform_exposure; 12 | private float lastOpacity = 0, lastExposure = 0; 13 | private final FloatBuffer transform; 14 | 15 | ModelRenderProgramOld(Shader vertexShader, Shader fragmentShader, int textureAtlas, ModelBuffers models) { 16 | super(vertexShader, fragmentShader, textureAtlas, models); 17 | this.uniform_opacity = getUniformLocation("opacity"); 18 | this.uniform_transform = getUniformLocation("transform"); 19 | this.uniform_exposure = getUniformLocation("exposure"); 20 | this.transform = BufferUtils.createFloatBuffer(16); 21 | } 22 | 23 | @Override 24 | public void queueRender(Model model, float opacity, 25 | float posX, float posY, float scaleX, float scaleY, 26 | float rotation) { 27 | queueRender(model, opacity, 1.0f, posX, posY, scaleX, scaleY, rotation); 28 | } 29 | 30 | @Override 31 | public void queueRender(Model model, float opacity, float exposure, 32 | float posX, float posY, float scaleX, float scaleY, 33 | float rotation) { 34 | 35 | //TODO optimize uniforms with a uniform buffer object 36 | if(opacity != lastOpacity) { 37 | setFloat(uniform_opacity, opacity); 38 | lastOpacity = opacity; 39 | } 40 | 41 | if(exposure != lastExposure) { 42 | setFloat(uniform_exposure, exposure); 43 | lastExposure = exposure; 44 | } 45 | 46 | glUniformMatrix4fv(uniform_transform, false, 47 | prepInstanceData(transform, posX, posY, scaleX, scaleY, rotation).flip()); 48 | transform.clear(); 49 | ModelAttributes modelAttribs = (ModelAttributes)model; 50 | glBindVertexArray(vaoID); 51 | glDrawElements(GL_TRIANGLES, modelAttribs.indexCount(), GL_UNSIGNED_INT, modelAttribs.indexOffset() * Integer.BYTES); 52 | } 53 | 54 | private static FloatBuffer prepInstanceData(FloatBuffer dest, float posX, float posY, 55 | float scaleX, float scaleY, float rotation) { 56 | float rotX = (float)Math.cos(rotation); 57 | float rotY = (float)Math.sin(rotation); 58 | return dest 59 | .put(scaleX * rotX) .put(scaleX * -rotY).put(0).put(0) //column major 60 | .put(scaleY * rotY) .put(scaleY * rotX) .put(0).put(0) 61 | .put(0) .put(0) .put(0).put(0) 62 | .put(posX) .put(posY) .put(0).put(1); 63 | } 64 | 65 | @Override 66 | public void flush() {} 67 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ModelRenderer.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public interface ModelRenderer { 4 | void prepare(); 5 | void loadViewMatrixIdentity(); 6 | void loadViewMatrixScale(float scale, int framebufferWidth, int framebufferHeight); 7 | void loadViewMatrixStandard(int framebufferWidth, int framebufferHeight); 8 | void loadViewMatrix(Camera camera, int framebufferWidth, int framebufferHeight); 9 | void queueRender(Model model, float opacity, float posX, float posY, float scaleX, float scaleY, float rotation); 10 | void queueRender(Model model, float opacity, float exposure, float posX, float posY, float scaleX, float scaleY, float rotation); 11 | void flush(); 12 | void delete(); 13 | //TODO add method to get the rectangle bounds of each model (width and height, computed from vertices) 14 | } 15 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ModelStorage.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.FloatBuffer; 4 | import java.nio.IntBuffer; 5 | import org.lwjgl.BufferUtils; 6 | 7 | import static org.lwjgl.opengl.GL46C.*; 8 | 9 | public class ModelStorage { 10 | protected static final int POSITION_SIZE = 2; 11 | protected static final int TEXTURE_COORD_SIZE = 2; 12 | 13 | static record ModelAttributes( 14 | int indexOffset, 15 | int indexCount, float width, float height) implements Model {} 16 | 17 | public static record ModelData( 18 | ModelDestination out, 19 | float width, float height, 20 | float[] positions, 21 | float[] textureCoords, 22 | int[] indices) { 23 | public int indexCount() { 24 | return indices.length; 25 | } 26 | } 27 | 28 | public static record ModelBuffers(int positionsVBO, int textureCoordsVBO, int indicesVBO) {} 29 | 30 | public static ModelBuffers uploadModels(Iterable models) { 31 | //count totals 32 | int indexTotal = 0; 33 | int vertexTotal = 0; 34 | for(ModelData model : models) { 35 | if(model.positions().length / POSITION_SIZE != model.textureCoords().length / TEXTURE_COORD_SIZE) 36 | throw new IllegalArgumentException("vertex count mismatch"); 37 | indexTotal += model.indexCount(); 38 | vertexTotal += model.positions().length / POSITION_SIZE; 39 | } 40 | 41 | //TODO remove repeat vertices (same position and texture coord) and readjust indices? 42 | //initialize temporary buffers 43 | FloatBuffer positionData = BufferUtils.createFloatBuffer(vertexTotal * POSITION_SIZE); 44 | FloatBuffer textureCoordsData = BufferUtils.createFloatBuffer(vertexTotal * TEXTURE_COORD_SIZE); 45 | IntBuffer indexData = BufferUtils.createIntBuffer(indexTotal); 46 | 47 | for(ModelData model : models) { 48 | int vertexOffset = positionData.position() / POSITION_SIZE; 49 | positionData.put(model.positions()); 50 | textureCoordsData.put(model.textureCoords()); 51 | 52 | int indexOffset = indexData.position(); 53 | for(int index : model.indices()) { 54 | indexData.put(vertexOffset + index); //adjust index to be absolute instead of relative to model 55 | } 56 | model.out().set(new ModelAttributes(indexOffset, model.indexCount(), model.width(), model.height())); 57 | } 58 | 59 | //TODO interleave positions and texture coordinates, and make it possible to add more components with more dynamic placement 60 | int positionsID = glGenBuffers(); 61 | glBindBuffer(GL_ARRAY_BUFFER, positionsID); 62 | glBufferStorage(GL_ARRAY_BUFFER, positionData.flip(), 0); 63 | 64 | int textureCoordsID = glGenBuffers(); 65 | glBindBuffer(GL_ARRAY_BUFFER, textureCoordsID); 66 | glBufferStorage(GL_ARRAY_BUFFER, textureCoordsData.flip(), 0); 67 | 68 | int indicesID = glGenBuffers(); 69 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesID); 70 | glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, indexData.flip(), 0); 71 | 72 | GraphicsUtility.checkErrors(); 73 | 74 | return new ModelBuffers(positionsID, textureCoordsID, indicesID); 75 | } 76 | 77 | public static int defineVAO(ModelBuffers data, int attributePositions, int attributeTextureCoords) { 78 | int vao = glGenVertexArrays(); 79 | glBindVertexArray(vao); 80 | //TODO interleave positions and texture coordinates, and make it possible to add more components with more dynamic placement 81 | //can also use DSA functions glBindVertexBuffer or glVertexArrayVertexBuffers 82 | glBindBuffer(GL_ARRAY_BUFFER, data.positionsVBO); 83 | glEnableVertexAttribArray(attributePositions); 84 | glVertexAttribPointer(attributePositions, POSITION_SIZE, GL_FLOAT, false, 0, 0); 85 | glBindBuffer(GL_ARRAY_BUFFER, data.textureCoordsVBO); 86 | glEnableVertexAttribArray(attributeTextureCoords); 87 | glVertexAttribPointer(attributeTextureCoords, TEXTURE_COORD_SIZE, GL_FLOAT, false, 0, 0); 88 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, data.indicesVBO); 89 | glBindVertexArray(0); 90 | GraphicsUtility.checkErrors(); 91 | return vao; 92 | } 93 | 94 | public static int defineVAOPositionsOnly(ModelBuffers data, int attributePositions) { 95 | int vao = glGenVertexArrays(); 96 | glBindVertexArray(vao); 97 | //TODO interleave positions and texture coordinates, and make it possible to add more components with more dynamic placement 98 | glBindBuffer(GL_ARRAY_BUFFER, data.positionsVBO); 99 | glEnableVertexAttribArray(attributePositions); 100 | glVertexAttribPointer(attributePositions, POSITION_SIZE, GL_FLOAT, false, 0, 0); 101 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, data.indicesVBO); 102 | glBindVertexArray(0); 103 | GraphicsUtility.checkErrors(); 104 | return vao; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/MutableGraphics.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public class MutableGraphics implements Graphics { 4 | public Model model; 5 | public float opacity, rotation, scaleX, scaleY; 6 | 7 | public MutableGraphics(Model modelIndex, float scaleX, float scaleY, float rotation, float opacity) { 8 | this.model = modelIndex; 9 | this.opacity = opacity; 10 | this.scaleX = scaleX; 11 | this.scaleY = scaleY; 12 | this.rotation = rotation; 13 | } 14 | 15 | @Override 16 | public final Model getModel() { 17 | return model; 18 | } 19 | 20 | @Override 21 | public final float getOpacity() { 22 | return opacity; 23 | } 24 | 25 | @Override 26 | public final float getScaleX() { 27 | return scaleX; 28 | } 29 | 30 | @Override 31 | public final float getScaleY() { 32 | return scaleY; 33 | } 34 | 35 | @Override 36 | public final float getRotation() { 37 | return rotation; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/OpenGLByteTexture.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.ByteBuffer; 4 | import org.lwjgl.system.MemoryStack; 5 | 6 | import static org.lwjgl.opengl.GL46C.*; 7 | /** 8 | * Represents an OpenGL texture 9 | * UNUSED TODO delete! 10 | */ 11 | public final class OpenGLByteTexture extends OpenGLTexture { 12 | public OpenGLByteTexture(ByteBuffer data, int width, int height) { 13 | super(glGenTextures()); 14 | setup(id); 15 | glTexImage2D(GL_TEXTURE_2D, 0, GL_R8UI, width, height, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, data); 16 | GraphicsUtility.checkErrors(); 17 | } 18 | 19 | public OpenGLByteTexture(int width, int height) { 20 | super(glGenTextures()); 21 | setup(id); 22 | setSize(width, height); 23 | } 24 | 25 | @Override 26 | public void setSize(int width, int height) { 27 | glBindTexture(GL_TEXTURE_2D, id); 28 | glTexImage2D(GL_TEXTURE_2D, 0, GL_R8UI, width, height, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 0); 29 | } 30 | 31 | public void setPixel(int x, int y, byte value) { 32 | try(MemoryStack stack = MemoryStack.stackPush()) { 33 | ByteBuffer val = stack.bytes(value); 34 | glTextureSubImage2D(id, 0, x, y, 1, 1, GL_RED_INTEGER, GL_UNSIGNED_BYTE, val); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/OpenGLException.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public class OpenGLException extends RuntimeException { 4 | 5 | public OpenGLException() { 6 | super(); 7 | } 8 | 9 | public OpenGLException(String message) { 10 | super(message); 11 | } 12 | 13 | public OpenGLException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public OpenGLException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/OpenGLTexture.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | import static org.lwjgl.opengl.GL11C.*; 6 | import static org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE; 7 | 8 | /** 9 | * Represents an OpenGL texture 10 | * UNUSED TODO delete! 11 | */ 12 | public class OpenGLTexture { 13 | public final int id; 14 | 15 | public OpenGLTexture(int id) { 16 | this.id = id; 17 | } 18 | 19 | public OpenGLTexture(ByteBuffer pixels, int width, int height) { 20 | this.id = glGenTextures(); 21 | setup(id); 22 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); 23 | glBindTexture(GL_TEXTURE_2D, 0); 24 | GraphicsUtility.checkErrors(); 25 | } 26 | 27 | public OpenGLTexture(int width, int height) { 28 | this.id = glGenTextures(); 29 | setup(id); 30 | setSize(width, height); 31 | } 32 | 33 | public void setSize(int width, int height) { 34 | glBindTexture(GL_TEXTURE_2D, id); 35 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); 36 | } 37 | 38 | protected static void setup(int texture) { 39 | glBindTexture(GL_TEXTURE_2D, texture); 40 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 41 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 42 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 43 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 44 | } 45 | 46 | public void bind() { 47 | glBindTexture(GL_TEXTURE_2D, id); 48 | } 49 | 50 | public void delete() { 51 | glDeleteTextures(id); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Renderable.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public interface Renderable { 4 | //TODO make a special EntityRenderer interface with rendering commads that are controlled by the world renderer 5 | void render(ModelRenderer program, float exposure); 6 | float getWidth(); 7 | float getHeight(); 8 | } 9 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Shader.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.IntBuffer; 5 | import org.lwjgl.system.MemoryStack; 6 | 7 | import static org.lwjgl.opengl.GL46C.*; 8 | 9 | public class Shader { 10 | private final int shaderID; 11 | 12 | /** Type of OpenGL shader **/ 13 | public enum ShaderType { 14 | VERTEX(GL_VERTEX_SHADER), 15 | FRAGMENT(GL_FRAGMENT_SHADER), 16 | GEOMETRY(GL_GEOMETRY_SHADER), 17 | COMPUTE(GL_COMPUTE_SHADER); 18 | 19 | private final int glType; 20 | 21 | ShaderType(int glType) { 22 | this.glType = glType; 23 | } 24 | } 25 | 26 | public static Shader fromSource(String programSource, ShaderType type) { 27 | int shaderID = glCreateShader(type.glType); 28 | glShaderSource(shaderID, programSource); 29 | glCompileShader(shaderID); 30 | Shader shader = new Shader(shaderID); 31 | GraphicsUtility.checkShaderCompilation(shader); 32 | return shader; 33 | } 34 | 35 | public static Shader fromSPIRV(ByteBuffer moduleBinary, ShaderType type) { 36 | int shaderID = glCreateShader(type.glType); 37 | try(MemoryStack stack = MemoryStack.stackPush()) { 38 | glShaderBinary(stack.ints(shaderID), GL_SHADER_BINARY_FORMAT_SPIR_V, moduleBinary); 39 | IntBuffer empty = stack.mallocInt(0); 40 | glSpecializeShader(shaderID, "main", empty, empty); 41 | } 42 | Shader shader = new Shader(shaderID); 43 | GraphicsUtility.checkShaderCompilation(shader); 44 | return shader; 45 | } 46 | 47 | private Shader(int shaderID) { 48 | this.shaderID = shaderID; 49 | } 50 | 51 | public int getShaderID() { 52 | return shaderID; 53 | } 54 | 55 | public void delete() { 56 | glDeleteShader(shaderID); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ShaderProgram.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import static org.lwjgl.opengl.GL46C.*; 4 | 5 | public class ShaderProgram { 6 | protected final int programID; 7 | protected final Shader[] shaders; //stores the shaders for deletion 8 | 9 | public ShaderProgram(Shader... shaders) { 10 | this.shaders = shaders; 11 | programID = glCreateProgram(); 12 | 13 | for(Shader s : shaders) { 14 | glAttachShader(programID, s.getShaderID()); 15 | GraphicsUtility.checkErrors(); 16 | } 17 | 18 | glLinkProgram(programID); 19 | glValidateProgram(programID); 20 | 21 | GraphicsUtility.checkProgramCompilation(this); 22 | } 23 | 24 | protected final void setCurrent() { 25 | glUseProgram(programID); 26 | } 27 | 28 | protected final int getAttributeLocation(String name) { 29 | return glGetAttribLocation(programID, name); 30 | } 31 | 32 | protected final int getUniformLocation(String name) { 33 | return glGetUniformLocation(programID, name); 34 | } 35 | 36 | /** 37 | * Put a texture in a texture unit 38 | * @param textureUnit the texture unit 39 | * @param textureID the texture 40 | */ 41 | public static void setCurrentTexture(int textureUnit, int textureID) { 42 | glActiveTexture(GL_TEXTURE0 + textureUnit); 43 | glBindTexture(GL_TEXTURE_2D, textureID); 44 | } 45 | 46 | public final void setMatrix(int uniformID, float[] values) { 47 | glProgramUniformMatrix4fv(programID, uniformID, true, values); 48 | } 49 | 50 | public final void setInteger(int uniformID, int value) { 51 | glProgramUniform1i(programID, uniformID, value); 52 | } 53 | 54 | public final void setFloat(int uniformID, float value) { 55 | glProgramUniform1f(programID, uniformID, value); 56 | } 57 | 58 | public final void setVectorUnsigned(int uniformID, int x, int y, int z) { 59 | glProgramUniform3ui(programID, uniformID, x, y, z); 60 | } 61 | 62 | public final void setVectorUnsigned(int uniformID, int x, int y) { 63 | glProgramUniform2ui(programID, uniformID, x, y); 64 | } 65 | 66 | public final void setVector(int uniformID, float x, float y, float z) { 67 | glProgramUniform3f(programID, uniformID, x, y, z); 68 | } 69 | 70 | public final void setVector(int uniformID, float x, float y, float z, float w) { 71 | glProgramUniform4f(programID, uniformID, x, y, z, w); 72 | } 73 | 74 | public final void setBoolean(int uniformID, boolean value) { 75 | glProgramUniform1i(programID, uniformID, value ? 1 : 0); 76 | } 77 | 78 | public final int getInteger(int uniformID) { 79 | return glGetUniformi(programID, uniformID); 80 | } 81 | 82 | public final float getFloat(int uniformID) { 83 | return glGetUniformf(programID, uniformID); 84 | } 85 | 86 | public final boolean getBoolean(int uniformID) { 87 | return glGetUniformi(programID, uniformID) == 1; 88 | } 89 | 90 | public void delete() { 91 | deleteShaders(); 92 | deleteProgram(); 93 | } 94 | 95 | public final void deleteProgram() { 96 | glDeleteProgram(programID); 97 | GraphicsUtility.checkErrors(); 98 | } 99 | 100 | public void deleteShaders() { 101 | for(Shader s : shaders) { 102 | glDetachShader(programID, s.getShaderID()); 103 | s.delete(); 104 | } 105 | GraphicsUtility.checkErrors(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ShadingApplyProgram.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import ritzow.sandbox.client.util.ClientUtility; 4 | 5 | import static org.lwjgl.opengl.GL46C.*; 6 | 7 | public class ShadingApplyProgram extends ShaderProgram { 8 | private static final int BLOCK_LIGHTING_TEXTURE_UNIT = GL_TEXTURE0; 9 | 10 | private final int vao, uniformView; 11 | 12 | public ShadingApplyProgram(Shader vertex, Shader geometry, Shader fragment) { 13 | super(vertex, geometry, fragment); 14 | vao = glGenVertexArrays(); 15 | uniformView = getUniformLocation("view"); 16 | } 17 | 18 | public void render(OpenGLTexture lighting, Camera camera, int width, int height) { 19 | setCurrent(); 20 | //TODO Could also do this more efficiently using a second color attachment when doing block model rendering so only fragments over blocks have light computed 21 | setVector(uniformView, 22 | ClientUtility.getViewLeftBound(camera, width, height), 23 | ClientUtility.getViewBottomBound(camera, width, height), 24 | ClientUtility.getViewRightBound(camera, width, height), 25 | ClientUtility.getViewTopBound(camera, width, height) 26 | ); 27 | glActiveTexture(BLOCK_LIGHTING_TEXTURE_UNIT); 28 | glBindTexture(GL_TEXTURE_2D, lighting.id); 29 | glBindVertexArray(vao); 30 | RenderManager.endColorCorrectOutput(); 31 | glDrawArrays(GL_POINTS, 0, 1); 32 | RenderManager.beginColorCorrectOutput(); 33 | } 34 | 35 | @Override 36 | public void delete() { 37 | super.delete(); 38 | glDeleteVertexArrays(vao); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/ShadingComputeProgram.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import static org.lwjgl.opengl.GL46C.*; 4 | 5 | public class ShadingComputeProgram extends ShaderProgram { 6 | private static final int 7 | DIFFUSE_IMAGE_UNIT = 0, 8 | SHADING_IMAGE_UNIT = 1; 9 | 10 | private final int uniformBounds; 11 | 12 | public ShadingComputeProgram(Shader compute) { 13 | super(compute); 14 | uniformBounds = getUniformLocation("bounds"); 15 | } 16 | 17 | public void render(OpenGLTexture diffuse, OpenGLTexture shading, int left, int bottom, int right, int top) { 18 | setCurrent(); 19 | left -= 2; 20 | bottom -= 2; 21 | right += 2; 22 | top += 2; 23 | setVectorUnsigned(uniformBounds, left, bottom); 24 | glBindImageTexture(DIFFUSE_IMAGE_UNIT, diffuse.id, 0, false, 0, GL_READ_ONLY, GL_R8UI); 25 | glBindImageTexture(SHADING_IMAGE_UNIT, shading.id, 0, false, 0, GL_WRITE_ONLY, GL_R8UI); 26 | glDispatchCompute(right - left, top - bottom, 1); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/StaticLight.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public class StaticLight extends Light { 4 | private final float red, green, blue; 5 | private final float posX, posY; 6 | private final float intensity; //TODO this is basically the radius 7 | 8 | public StaticLight(float posX, float posY, float r, float g, float b, float intensity) { 9 | this.posX = posX; 10 | this.posY = posY; 11 | this.red = r; 12 | this.green = g; 13 | this.blue = b; 14 | this.intensity = intensity; 15 | } 16 | 17 | @Override 18 | public float posX() { 19 | return posX; 20 | } 21 | 22 | @Override 23 | public float posY() { 24 | return posY; 25 | } 26 | 27 | @Override 28 | public float red() { 29 | return red; 30 | } 31 | 32 | @Override 33 | public float green() { 34 | return green; 35 | } 36 | 37 | @Override 38 | public float blue() { 39 | return blue; 40 | } 41 | 42 | @Override 43 | public float intensity() { 44 | return intensity; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/TextureData.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | public class TextureData implements Comparable { 4 | private final int width; 5 | private final int bytesPerPixel; 6 | private final byte[] data; 7 | 8 | /** 9 | * Stores information about a texture. 10 | * @param widthPixels the texture's width, in pixels 11 | * @param bytesPerPixel the number of bytes per pixel 12 | * @param data the texture data 13 | */ 14 | public TextureData(int widthPixels, int bytesPerPixel, byte[] data) { 15 | this.width = widthPixels; 16 | this.bytesPerPixel = bytesPerPixel; 17 | this.data = data; 18 | } 19 | 20 | /** 21 | * @return the texture's width, in pixels 22 | */ 23 | public int getWidth() { 24 | return width; 25 | } 26 | 27 | /** 28 | * @return the texture's height, in pixels 29 | */ 30 | public int getHeight() { 31 | return data.length / (bytesPerPixel * width); //total size / bytes per row 32 | } 33 | 34 | public byte[] getData() { 35 | return data; 36 | } 37 | 38 | public int getBytesPerPixel() { 39 | return bytesPerPixel; 40 | } 41 | 42 | private int pixelCount() { 43 | return getWidth() * getHeight(); 44 | } 45 | 46 | @Override 47 | public int compareTo(TextureData other) { 48 | return other.pixelCount() - pixelCount(); //images with more pixels come first 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "TextureData [width=" + width + ", bytesPerPixel=" + bytesPerPixel + ", data=" + data.length + " bytes]"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/graphics/Textures.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.graphics; 2 | 3 | import de.matthiasmann.twl.utils.PNGDecoder; 4 | import de.matthiasmann.twl.utils.PNGDecoder.Format; 5 | import java.io.IOException; 6 | import java.nio.ByteBuffer; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import org.lwjgl.BufferUtils; 10 | import ritzow.sandbox.client.data.StandardClientProperties; 11 | 12 | public final class Textures { 13 | 14 | public static TextureAtlas buildAtlas(TextureData... textures) { 15 | return new TextureAtlas(textures); 16 | } 17 | 18 | public static TextureData loadTextureName(String name) throws IOException { 19 | return loadTextureData(StandardClientProperties.TEXTURES_PATH.resolve(name + ".png")); 20 | } 21 | 22 | public static TextureData loadTextureData(Path file) throws IOException { 23 | try(var in = Files.newInputStream(file)) { 24 | PNGDecoder decoder = new PNGDecoder(in); 25 | ByteBuffer pixels = BufferUtils.createByteBuffer(decoder.getWidth() * decoder.getHeight() * 4); 26 | decoder.decode(pixels, decoder.getWidth() * 4, Format.RGBA); 27 | byte[] data = new byte[pixels.flip().remaining()]; 28 | pixels.get(data); 29 | return new TextureData(decoder.getWidth(), 4, data); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/input/Button.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.input; 2 | 3 | public record Button(byte type, int code) implements Control { 4 | public boolean equals(byte type, int code) { 5 | return this.type == type && this.code == code; 6 | } 7 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/input/Control.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.input; 2 | 3 | import static org.lwjgl.glfw.GLFW.*; 4 | import static ritzow.sandbox.client.data.StandardClientOptions.LEFTY; 5 | 6 | public interface Control { 7 | byte BUTTON_TYPE_MOUSE = 0, BUTTON_TYPE_KEYBOARD = 1; 8 | 9 | byte type(); 10 | int code(); 11 | 12 | Button UI_ACTIVATE = new Button(BUTTON_TYPE_MOUSE, GLFW_MOUSE_BUTTON_LEFT); 13 | Button UI_CONTEXT = new Button(BUTTON_TYPE_MOUSE, GLFW_MOUSE_BUTTON_RIGHT); 14 | Button UI_TERTIARY = new Button(BUTTON_TYPE_MOUSE, GLFW_MOUSE_BUTTON_MIDDLE); 15 | Button UI_BACKSPACE = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_BACKSPACE); 16 | Button UI_DELETE = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_DELETE); 17 | Button UI_CONFIRM = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_ENTER); 18 | 19 | Button QUIT = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_ESCAPE); 20 | Button FULLSCREEN = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_F11); 21 | 22 | Button ZOOM_INCREASE = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_EQUAL); 23 | Button ZOOM_DECREASE = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_MINUS); 24 | Button ZOOM_RESET = new Button(BUTTON_TYPE_MOUSE, GLFW_MOUSE_BUTTON_MIDDLE); 25 | 26 | Button SCROLL_MODIFIER = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_RIGHT_SHIFT); 27 | Button USE_HELD_ITEM = new Button(BUTTON_TYPE_MOUSE, GLFW_MOUSE_BUTTON_LEFT); 28 | Button THROW_BOMB = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_ENTER); 29 | 30 | Button MOVE_UP = new Button(BUTTON_TYPE_KEYBOARD, LEFTY ? GLFW_KEY_UP : GLFW_KEY_W); 31 | Button MOVE_DOWN = new Button(BUTTON_TYPE_KEYBOARD, LEFTY ? GLFW_KEY_DOWN : GLFW_KEY_S); 32 | Button MOVE_LEFT = new Button(BUTTON_TYPE_KEYBOARD, LEFTY ? GLFW_KEY_LEFT : GLFW_KEY_A); 33 | Button MOVE_RIGHT = new Button(BUTTON_TYPE_KEYBOARD, LEFTY ? GLFW_KEY_RIGHT : GLFW_KEY_D); 34 | 35 | Button SLOT_SELECT_1 = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_KP_1); 36 | Button SLOT_SELECT_2 = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_KP_2); 37 | Button SLOT_SELECT_3 = new Button(BUTTON_TYPE_KEYBOARD, GLFW_KEY_KP_3); 38 | } 39 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/input/ControlsQuery.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.input; 2 | 3 | public interface ControlsQuery { 4 | boolean isPressed(Button control); 5 | boolean isNewlyPressed(Button control); 6 | boolean isNewlyReleased(Button control); 7 | State state(Button control); 8 | //TODO could potentially make state modification a thing, so something can disable/"consume" a control, like a UI element 9 | //TODO implement mouse scroll polling in ControlsQuery 10 | 11 | enum State { 12 | PREVIOUSLY_PRESSED, 13 | PREVIOUSLY_RELEASED, 14 | NEWLY_PRESSED, 15 | NEWLY_RELEASED 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/input/InputProvider.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.input; 2 | 3 | /** Queries input **/ 4 | public interface InputProvider { 5 | boolean isControlActivated(Button control); 6 | int getCursorX(); 7 | int getCursorY(); 8 | } 9 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/input/controller/TrackingCameraController.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.input.controller; 2 | 3 | import ritzow.sandbox.client.audio.AudioSystem; 4 | import ritzow.sandbox.client.graphics.Camera; 5 | import ritzow.sandbox.client.input.Control; 6 | import ritzow.sandbox.client.input.ControlsQuery; 7 | import ritzow.sandbox.util.Utility; 8 | import ritzow.sandbox.world.entity.Entity; 9 | 10 | public final class TrackingCameraController { 11 | private final Camera camera; 12 | private final float zoomSpeedNanos, minZoom, maxZoom; 13 | 14 | public TrackingCameraController(float zoomSpeed, float minZoom, float maxZoom) { 15 | this.camera = new Camera(0, 0, minZoom); 16 | this.zoomSpeedNanos = Utility.convertPerSecondToPerNano(zoomSpeed); 17 | this.minZoom = minZoom; 18 | this.maxZoom = maxZoom; 19 | } 20 | 21 | public void update(ControlsQuery controls, Entity target, AudioSystem audio, long nanoseconds) { 22 | if(controls.isNewlyPressed(Control.ZOOM_RESET)) { 23 | resetZoom(); 24 | } else if(controls.isPressed(Control.ZOOM_INCREASE)) { 25 | computeZoom(zoomSpeedNanos, nanoseconds); 26 | } else if(controls.isPressed(Control.ZOOM_DECREASE)) { 27 | computeZoom(-zoomSpeedNanos, nanoseconds); 28 | } 29 | 30 | float posX = target.getPositionX(); 31 | float posY = target.getPositionY(); 32 | camera.setPositionX(posX); 33 | camera.setPositionY(posY); 34 | audio.setPosition(posX, posY); 35 | } 36 | 37 | public void resetZoom() { 38 | camera.setZoom(Math.fma(0.1f, maxZoom, minZoom)); 39 | } 40 | 41 | public void zoom(float amount) { 42 | computeZoom(0.2f, amount); 43 | } 44 | 45 | private void computeZoom(float scale, float offset) { 46 | camera.setZoom(Utility.clamp(minZoom, camera.getZoom() * Math.fma(scale, offset, 1.0f), maxZoom)); 47 | } 48 | 49 | public Camera getCamera() { 50 | return camera; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/network/GameTalker.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.network; 2 | 3 | public interface GameTalker { 4 | void sendBlockBreak(int x, int y); 5 | void sendBlockPlace(int x, int y); 6 | void sendBombThrow(float angle); 7 | } 8 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/network/ServerBadDataException.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.network; 2 | 3 | public class ServerBadDataException extends RuntimeException { 4 | public ServerBadDataException() { 5 | super(); 6 | } 7 | 8 | public ServerBadDataException(String message) { 9 | super(message); 10 | } 11 | 12 | public ServerBadDataException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public ServerBadDataException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | protected ServerBadDataException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/Animation.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | public interface Animation { 4 | void update(long progressNanos, float completionRatio, long nanosSinceLast); 5 | default void onEnd() {} 6 | } 7 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/Circle.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | import ritzow.sandbox.util.Utility; 4 | 5 | public record Circle(float radius) implements Shape { 6 | @Override 7 | public Rectangle toRectangle() { 8 | return new Rectangle(radius * 2, radius * 2); 9 | } 10 | 11 | @Override 12 | public Shape scale(float scale) { 13 | return new Circle(radius * scale); 14 | } 15 | 16 | @Override 17 | public boolean intersects(Position point) { 18 | return Utility.withinDistance(point.x(), point.y(), 0, 0, radius); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/Font.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | import java.util.Arrays; 4 | import java.util.function.Consumer; 5 | import ritzow.sandbox.client.graphics.Model; 6 | import ritzow.sandbox.client.graphics.ModelStorage.ModelData; 7 | import ritzow.sandbox.client.graphics.RenderManager; 8 | import ritzow.sandbox.client.graphics.TextureAtlas; 9 | import ritzow.sandbox.client.graphics.TextureAtlas.AtlasRegion; 10 | import ritzow.sandbox.client.graphics.TextureData; 11 | 12 | public final class Font { 13 | 14 | private static final int[] CHARACTER_LIST = { 15 | '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 16 | ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 17 | 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 18 | 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', ' ', 'μ' 19 | }; 20 | 21 | //TODO some font characters are not in correct ascii order, such as the space character 22 | 23 | public static Font load(double texelWidth, int glyphsPerRow, int glyphWidth, int glyphHeight, int paddingSize, 24 | int textureWidth, int textureHeight, Consumer out, int[] indices, float startX, float startY, float endX, float endY) { 25 | return new Font(texelWidth, glyphsPerRow, glyphWidth, glyphHeight, paddingSize, textureWidth, textureHeight, out, indices, startX, startY, endX, endY); 26 | } 27 | 28 | public static Font load(TextureData fontTexture, TextureAtlas atlas, AtlasRegion region, Consumer modelOut) { 29 | return new Font(1.0d / atlas.width(), 25, 9, 9, 1, fontTexture.getWidth(), fontTexture.getHeight(), modelOut, 30 | RenderManager.VERTEX_INDICES_RECT, region.leftX(), region.bottomY(), region.rightX(), region.topY()); 31 | } 32 | 33 | private final Model[] glyphs; 34 | private final int glyphWidth, glyphHeight; 35 | 36 | public int getGlyphWidth() { 37 | return glyphWidth; 38 | } 39 | 40 | public int getGlyphHeight() { 41 | return glyphHeight; 42 | } 43 | 44 | public Font(double texelDimension, int glyphsPerRow, int glyphWidth, int glyphHeight, int paddingSize, int textureWidth, int textureHeight, 45 | Consumer out, int[] indices, float leftX, float bottomY, float rightX, float topY) { 46 | glyphs = new Model[Arrays.stream(CHARACTER_LIST).max().orElseThrow() + 1]; 47 | this.glyphWidth = glyphWidth; 48 | this.glyphHeight = glyphHeight; 49 | double texelWidthHalf = texelDimension/textureWidth/2d; 50 | double texelHeightHalf = texelDimension/textureHeight/2d; 51 | double width = (double)glyphWidth / textureWidth * (rightX - leftX); 52 | double height = (double)glyphHeight / textureHeight * (topY - bottomY); 53 | double paddingX = (double)paddingSize / textureWidth * (rightX - leftX); 54 | double paddingY = (double)paddingSize / textureHeight * (topY - bottomY); 55 | for(int i = 0; i < CHARACTER_LIST.length; i++) { 56 | int row = i / glyphsPerRow; 57 | int column = i % glyphsPerRow; 58 | double topInitial = topY - paddingY - (paddingY + height) * row; 59 | double leftInitial = leftX + paddingX + (paddingX + width) * column; 60 | float top = (float)(topInitial - texelHeightHalf); 61 | float left = (float)(leftInitial + texelWidthHalf); 62 | float bottom = (float)(topInitial - height + texelHeightHalf); 63 | float right = (float)(leftInitial + width - texelWidthHalf); 64 | 65 | float[] coords = new float[] { 66 | left, top, 67 | left, bottom, 68 | right, bottom, 69 | right, top 70 | }; 71 | 72 | float halfWidth = (float)(glyphWidth/2d); 73 | float halfHeight = (float)(glyphHeight/2d); 74 | float[] positions = { 75 | -halfWidth, halfHeight, 76 | -halfWidth, -halfHeight, 77 | halfWidth, -halfHeight, 78 | halfWidth, halfHeight 79 | }; 80 | 81 | int charIndex = i; 82 | out.accept(new ModelData(model -> glyphs[CHARACTER_LIST[charIndex]] = model, glyphWidth, glyphHeight, positions, coords, indices)); 83 | } 84 | } 85 | 86 | public Model getModel(int character) { 87 | return character < glyphs.length && character > 0 ? glyphs[character] : null; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/GuiElement.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | /** A GuiElement is always rendered centered at coordinates [0, 0] with a shape centered at [0, 0] 4 | * Shape offsets should be achieved with child GuiElements. **/ 5 | public interface GuiElement { 6 | /** Render without mouse hover **/ 7 | void render(GuiRenderer renderer, long nanos); 8 | 9 | //TODO add a gui element EmbeddedRenderer that does a flush and switch to a potentially different shader program and parameters, then switches back after. 10 | 11 | /** The visual shape and size of this GUI element, composite elements should 12 | * compute this using the size of their children and should use it for computing positions. 13 | * If the GuiElement requires parent bounds to compute its own, 14 | * this should return some default or minimum shape **/ 15 | Shape shape(); 16 | 17 | //TODO this is called even when the parent is dynamically sized 18 | /** Called when the parent's shape is not bounded by its children **/ 19 | default Shape shape(Rectangle parent) { 20 | return shape(); 21 | } 22 | 23 | //default boolean isSizedByChildren() 24 | 25 | //TODO use shape to cull drawing of GuiElements (ie they are outside of the UI) and possibly for scissoring. 26 | // record Scissor(float width, float height) {} 27 | // default Scissor scissor() { 28 | // return null; 29 | // } 30 | } 31 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/GuiRenderer.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | import java.time.Duration; 4 | import java.util.Optional; 5 | import ritzow.sandbox.client.graphics.Model; 6 | import ritzow.sandbox.client.input.ControlsQuery; 7 | 8 | /** 9 | * The GuiRenderer interface renders GUI elements using the standard coordinate space of [-1, 1] TODO this isn't correct, need to involve framebuffer pixel sizes 10 | * in the X and Y dimensions to indicate positions within the game window. Nested draws will 11 | * be affected by parent element transformations and opacities. 12 | */ 13 | public interface GuiRenderer { 14 | /** Draws a GUI element relative to its parent **/ 15 | void draw(Model model, float opacity, float posX, float posY, float scaleX, float scaleY, float rotation); 16 | 17 | /** Draws a GUI element relative to its parent with no extra transformation **/ 18 | void draw(GuiElement element); 19 | 20 | /** Draws a GUI element relative to its parent with no relative opacity, scaling, or rotation **/ 21 | void draw(GuiElement element, float posX, float posY); 22 | 23 | /** Draws a GUI element relative to its parent with no rotation **/ 24 | void draw(GuiElement element, float opacity, float posX, float posY, float scaleX, float scaleY); 25 | 26 | /** Draws a GUI element relative to its parent **/ 27 | void draw(GuiElement element, float opacity, float posX, float posY, float scaleX, float scaleY, float rotation); 28 | 29 | void play(Animation animation, Duration duration); 30 | 31 | Rectangle parent(); 32 | ControlsQuery controls(); 33 | Optional mousePos(); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/Position.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | public final record Position(float x, float y) { 4 | 5 | public static Position of(float x, float y) { 6 | return new Position(x, y); 7 | } 8 | 9 | public Position translate(float x, float y) { 10 | return new Position(this.x + x, this.y + y); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/Rectangle.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | public final record Rectangle(float width, float height) implements Shape { 4 | 5 | @Override 6 | public Rectangle toRectangle() { 7 | return this; 8 | } 9 | 10 | @Override 11 | public Shape scale(float scale) { 12 | return new Rectangle(width * scale, height * scale); 13 | } 14 | 15 | @Override 16 | public boolean intersects(Position point) { 17 | return Math.abs(point.x()) <= width/2 && Math.abs(point.y()) <= height/2; 18 | } 19 | } -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/Shape.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui; 2 | 3 | public /*sealed*/ interface Shape /*permits Rectangle, InfinitePlane*/ { 4 | Rectangle toRectangle(); 5 | Shape scale(float scale); 6 | boolean intersects(Position point); 7 | } 8 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/AbsoluteGuiPositioner.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | import ritzow.sandbox.client.ui.*; 8 | 9 | public class AbsoluteGuiPositioner implements GuiElement { 10 | private static record ElementPos(GuiElement element, Position pos) {} 11 | private final List elements; 12 | private final Rectangle bounds; 13 | 14 | @SafeVarargs 15 | public AbsoluteGuiPositioner(Entry... elements) { 16 | this.elements = new ArrayList<>(elements.length); 17 | float leftX = 0, rightX = 0, bottomY = 0, topY = 0; 18 | //float width = 0, height = 0; 19 | for(var entry : elements) { 20 | Rectangle rect = entry.getKey().shape().toRectangle(); 21 | if(rect != null) { 22 | leftX = Math.min(entry.getValue().x() - rect.width()/2, leftX); 23 | rightX = Math.max(entry.getValue().x() + rect.width()/2, rightX); 24 | bottomY = Math.min(entry.getValue().y() - rect.height()/2, bottomY); 25 | topY = Math.max(entry.getValue().y() + rect.height()/2, topY); 26 | //width = Math.max(Math.abs(entry.getValue().y()) + rect.width()/2, width); 27 | //height = Math.max(Math.abs(entry.getValue().y()) + rect.height()/2, height); 28 | } 29 | this.elements.add(new ElementPos(entry.getKey(), entry.getValue())); 30 | } 31 | this.bounds = new Rectangle(Math.max(Math.abs(leftX), Math.abs(rightX)) * 2, Math.max(Math.abs(bottomY), Math.abs(topY)) * 2); 32 | } 33 | 34 | public static Entry alignLeft(GuiElement element, float x, float y) { 35 | Rectangle r = element.shape().toRectangle(); 36 | if(r != null) { 37 | return Map.entry(element, Position.of(x + r.width()/2, 0)); 38 | } else return Map.entry(element, Position.of(x, y)); 39 | } 40 | 41 | public static Entry alignCenter(GuiElement element, float x, float y) { 42 | return Map.entry(element, Position.of(x, y)); 43 | } 44 | 45 | public static Entry alignRight(GuiElement element, float x, float y) { 46 | throw new UnsupportedOperationException("not implemented yet"); 47 | } 48 | 49 | @Override 50 | public void render(GuiRenderer renderer, long nanos) { 51 | for(ElementPos element : elements) { 52 | renderer.draw(element.element, element.pos.x(), element.pos.y()); 53 | } 54 | } 55 | 56 | @Override 57 | public Shape shape() { 58 | return bounds; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/BorderAnchor.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.ui.*; 4 | 5 | public class BorderAnchor implements GuiElement { 6 | 7 | //TODO add corners as well, maybe remove the other offset value 8 | public enum Side { 9 | LEFT { 10 | @Override 11 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 12 | return Position.of((bounds.width() - border.width())/2f + x, y); 13 | } 14 | }, 15 | 16 | RIGHT { 17 | @Override 18 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 19 | return Position.of((border.width() - bounds.width())/2f - x, y); 20 | } 21 | }, 22 | 23 | BOTTOM { 24 | @Override 25 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 26 | return Position.of(x, (bounds.height() - border.height())/2f + y); 27 | } 28 | }, 29 | 30 | TOP { 31 | @Override 32 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 33 | return Position.of(x, (border.height() - bounds.height())/2f - y); 34 | } 35 | }, 36 | 37 | BOTTOM_LEFT { 38 | @Override 39 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 40 | return Position.of((bounds.width() - border.width())/2f + x, (bounds.height() - border.height())/2f + y); 41 | } 42 | }, 43 | 44 | BOTTOM_RIGHT { 45 | @Override 46 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 47 | return Position.of((border.width() - bounds.width())/2f - x, (bounds.height() - border.height())/2f + y); 48 | } 49 | }, 50 | 51 | TOP_LEFT { 52 | @Override 53 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 54 | return Position.of((bounds.width() - border.width())/2f + x, (border.height() - bounds.height())/2f - y); 55 | } 56 | }, 57 | 58 | TOP_RIGHT { 59 | @Override 60 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 61 | return Position.of((border.width() - bounds.width())/2f - x, (border.height() - bounds.height())/2f - y); 62 | } 63 | }, 64 | 65 | CENTER { 66 | @Override 67 | Position translate(float x, float y, Rectangle bounds, Rectangle border) { 68 | return Position.of(x, y); 69 | } 70 | }; 71 | 72 | abstract Position translate(float x, float y, Rectangle bounds, Rectangle border); 73 | } 74 | 75 | public record Anchor(GuiElement element, Side side, float x, float y) {} 76 | 77 | private final Anchor[] anchors; 78 | 79 | public BorderAnchor(Anchor... elements) { 80 | //TODO might be able to store positions as normalized in -1 to 1 space, then multiply by renderer.parent() bounds to get proper positions 81 | //however, this might not anchor them to the sides and instead scale them with them, not sure. 82 | this.anchors = elements; 83 | } 84 | 85 | @Override 86 | public void render(GuiRenderer renderer, long nanos) { 87 | Rectangle parent = renderer.parent(); 88 | for(Anchor anchor : anchors) { 89 | Rectangle bounds = anchor.element.shape(parent).toRectangle(); 90 | Position translated = anchor.side.translate(anchor.x, anchor.y, bounds, parent); 91 | renderer.draw(anchor.element, translated.x(), translated.y()); 92 | } 93 | } 94 | 95 | @Override 96 | public Shape shape() { 97 | return new Rectangle(1, 1); //TODO implement minimum size so elements don't overlap or which is pleasing to the eye 98 | } 99 | 100 | @Override 101 | public Shape shape(Rectangle parent) { 102 | return parent; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/Button.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.graphics.Model; 4 | import ritzow.sandbox.client.graphics.RenderManager; 5 | import ritzow.sandbox.client.input.Control; 6 | import ritzow.sandbox.client.ui.GuiElement; 7 | import ritzow.sandbox.client.ui.GuiRenderer; 8 | import ritzow.sandbox.client.ui.Shape; 9 | 10 | public class Button implements GuiElement { 11 | private final GuiElement content; 12 | private final Runnable action; 13 | private float scale = 1.0f; 14 | 15 | private static final float HOVER_SCALE = 1.1f, PRESS_SCALE = 0.9f, STANDARD_SCALE = 1.0f; 16 | 17 | public Button(GuiElement content, Runnable action) { 18 | this.content = content; 19 | this.action = action; 20 | } 21 | 22 | public Button(String text, Model model, Runnable action) { 23 | this.content = new HBox(0.1f, new Scaler(new Icon(model), 0.4f), new Text(text, RenderManager.FONT, 8, 0)); 24 | this.action = action; 25 | } 26 | 27 | @Override 28 | public void render(GuiRenderer renderer, long nanos) { 29 | //always use the original scale for the mouse click area size 30 | if(renderer.mousePos().map(content.shape()::intersects).orElse(false)) { 31 | float scale = switch(renderer.controls().state(Control.UI_ACTIVATE)) { 32 | case PREVIOUSLY_PRESSED, NEWLY_PRESSED -> PRESS_SCALE; 33 | 34 | case NEWLY_RELEASED -> { 35 | action.run(); 36 | yield 1; 37 | } 38 | 39 | default -> HOVER_SCALE; 40 | }; 41 | 42 | renderer.draw(content, 1.0f, 0.0f, 0.0f, scale, scale, 0.0f); 43 | this.scale = Math.max(1, scale); 44 | } else { 45 | renderer.draw(content); 46 | scale = 1; 47 | } 48 | 49 | // if(!anim) { 50 | // anim = true; 51 | // renderer.play(new Animation() { 52 | // @Override 53 | // public void update(long progressNanos, float completionRatio, long nanosSinceLast) { 54 | // //bouncing motion because it is half of sinusoid 55 | // float temp = Utility.convertRange(0, 1, 1, HOVER_SCALE, (float)Math.sin(Math.PI * completionRatio)); 56 | // scale = temp; 57 | // } 58 | // 59 | // @Override 60 | // public void onEnd() { 61 | // anim = false; 62 | // } 63 | // }, Duration.ofMillis(500)); 64 | // } 65 | } 66 | 67 | @Override 68 | public Shape shape() { 69 | return content.shape().scale(scale); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/EditableText.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import ritzow.sandbox.client.graphics.Model; 6 | import ritzow.sandbox.client.ui.*; 7 | 8 | public class EditableText implements GuiElement { 9 | private final List content; 10 | private final Font font; 11 | private final float scale, spacing; 12 | 13 | private static final float SIZE_SCALE = 0.002f; 14 | 15 | private static record Entry(Model model, int character) {} 16 | 17 | public EditableText(Font font, float size, float spacing) { 18 | this.font = font; 19 | this.scale = size * SIZE_SCALE; 20 | this.spacing = spacing; 21 | this.content = new ArrayList<>(); 22 | } 23 | 24 | @Override 25 | public void render(GuiRenderer renderer, long nanos) { 26 | float pos = -width()/2f; 27 | for(Entry e : content) { 28 | pos += charWidth()/2f; 29 | renderer.draw(e.model, 1, pos, 0, scale, scale, 0); 30 | pos += charWidth()/2f + spacing; 31 | } 32 | } 33 | 34 | private float charWidth() { 35 | return scale * font.getGlyphWidth(); 36 | } 37 | 38 | public boolean hasContent() { 39 | return !content.isEmpty(); 40 | } 41 | 42 | public int length() { 43 | return content.size(); 44 | } 45 | 46 | public void append(int character) { 47 | content.add(entry(character)); 48 | } 49 | 50 | public void append(String text) { 51 | text.codePoints().mapToObj(this::entry).forEachOrdered(content::add); 52 | } 53 | 54 | public void delete() { 55 | content.remove(content.size() - 1); 56 | } 57 | 58 | public void delete(int index) { 59 | content.remove(index); 60 | } 61 | 62 | public void insert(int index, int character) { 63 | content.add(index, entry(character)); 64 | } 65 | 66 | public void delete(int index, int count) { 67 | content.subList(index, index + count).clear(); 68 | } 69 | 70 | public void insert(int index, String text) { 71 | text.codePoints().mapToObj(this::entry).forEachOrdered(entry -> content.add(index, entry)); 72 | } 73 | 74 | //TODO only recompute when a character is added or removed 75 | private float width() { 76 | return font.getGlyphWidth() * content.size() * scale + Math.max(content.size() - 1, 0) * spacing; 77 | } 78 | 79 | private float height() { 80 | return font.getGlyphHeight() * scale; 81 | } 82 | 83 | public EditableText setContent(CharSequence text) { 84 | content.clear(); 85 | text.codePoints().mapToObj(this::entry).forEachOrdered(content::add); 86 | return this; 87 | } 88 | 89 | private Entry entry(int character) { 90 | return new Entry(getModel(character), character); 91 | } 92 | 93 | private Model getModel(int character) { 94 | Model model = font.getModel(character); 95 | if(model == null) { 96 | throw new IllegalArgumentException("Content string contains unknown character '" 97 | + Character.toString(character)); 98 | } 99 | return model; 100 | } 101 | 102 | public String content() { 103 | int[] codePoints = content.stream().mapToInt(Entry::character).toArray(); 104 | return new String(codePoints, 0, codePoints.length); 105 | } 106 | 107 | @Override 108 | public Shape shape() { 109 | return new Rectangle(width(), height()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/HBox.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.ui.GuiElement; 4 | import ritzow.sandbox.client.ui.GuiRenderer; 5 | import ritzow.sandbox.client.ui.Rectangle; 6 | import ritzow.sandbox.client.ui.Shape; 7 | 8 | public class HBox implements GuiElement { 9 | private final Alignment[] elements; 10 | private final Rectangle bounds; 11 | 12 | private static record Alignment(GuiElement e, float x) {} 13 | 14 | public HBox(float gap, GuiElement... elements) { 15 | this.elements = new Alignment[elements.length]; 16 | float width = (elements.length - 1) * gap, maxHeight = 0; 17 | for(GuiElement e : elements) { 18 | Rectangle rect = e.shape().toRectangle(); 19 | width += rect.width(); 20 | if(rect.height() > maxHeight) { 21 | maxHeight = rect.height(); 22 | } 23 | } 24 | 25 | float posX = -width/2; 26 | for(int i = 0; i < elements.length; i++) { 27 | GuiElement e = elements[i]; 28 | Rectangle bounds = e.shape().toRectangle(); 29 | posX += bounds.width()/2; 30 | this.elements[i] = new Alignment(e, posX); 31 | posX += bounds.width()/2 + gap; 32 | } 33 | 34 | this.bounds = new Rectangle(width, maxHeight); 35 | } 36 | 37 | @Override 38 | public void render(GuiRenderer renderer, long nanos) { 39 | for(Alignment e : elements) { 40 | renderer.draw(e.e, e.x, 0); 41 | } 42 | } 43 | 44 | @Override 45 | public Shape shape() { 46 | return bounds; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/HBoxDynamic.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import ritzow.sandbox.client.ui.GuiElement; 7 | import ritzow.sandbox.client.ui.GuiRenderer; 8 | import ritzow.sandbox.client.ui.Rectangle; 9 | 10 | public class HBoxDynamic implements GuiElement { 11 | private final List elements; 12 | private final float gap; 13 | 14 | public HBoxDynamic(float gap, GuiElement... elements) { 15 | this.elements = new ArrayList<>(Arrays.asList(elements)); 16 | this.gap = gap; 17 | } 18 | 19 | @Override 20 | public void render(GuiRenderer renderer, long nanos) { 21 | Rectangle bounds = shape(); 22 | float pos = -bounds.width()/2f; 23 | for(GuiElement e : elements) { 24 | Rectangle elementBounds = e.shape().toRectangle(); 25 | pos += elementBounds.width() / 2; 26 | renderer.draw(e, pos, 0); 27 | pos += elementBounds.width() / 2 + gap; 28 | } 29 | } 30 | 31 | @Override 32 | public Rectangle shape() { 33 | float width = (elements.size() - 1) * gap, maxHeight = 0; 34 | for(GuiElement e : elements) { 35 | Rectangle rect = e.shape().toRectangle(); 36 | width += rect.width(); 37 | if(rect.height() > maxHeight) { 38 | maxHeight = rect.height(); 39 | } 40 | } 41 | return new Rectangle(width, maxHeight); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/Holder.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import java.util.Objects; 4 | import ritzow.sandbox.client.ui.GuiElement; 5 | import ritzow.sandbox.client.ui.GuiRenderer; 6 | import ritzow.sandbox.client.ui.Rectangle; 7 | import ritzow.sandbox.client.ui.Shape; 8 | 9 | public class Holder implements GuiElement { 10 | private T element; 11 | 12 | public Holder(T element) { 13 | this.element = Objects.requireNonNull(element); 14 | } 15 | 16 | public T get() { 17 | return element; 18 | } 19 | 20 | public void set(T element) { 21 | this.element = Objects.requireNonNull(element); 22 | } 23 | 24 | @Override 25 | public void render(GuiRenderer renderer, long nanos) { 26 | if(element != null) { 27 | renderer.draw(element); 28 | } 29 | } 30 | 31 | @Override 32 | public Shape shape() { 33 | return element == null ? new Rectangle(0, 0) : element.shape(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/Icon.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.graphics.Model; 4 | import ritzow.sandbox.client.graphics.MutableGraphics; 5 | import ritzow.sandbox.client.ui.GuiElement; 6 | import ritzow.sandbox.client.ui.GuiRenderer; 7 | import ritzow.sandbox.client.ui.Rectangle; 8 | import ritzow.sandbox.client.ui.Shape; 9 | 10 | public class Icon implements GuiElement { 11 | private final MutableGraphics graphics; 12 | 13 | public Icon(Model modelID) { 14 | this.graphics = new MutableGraphics(modelID, 1.0f, 1.0f, 0, 1.0f); 15 | } 16 | 17 | @Override 18 | public void render(GuiRenderer renderer, long nanos) { 19 | renderer.draw(graphics.model, graphics.opacity, 0, 0, graphics.scaleX, graphics.scaleY, graphics.rotation); 20 | } 21 | 22 | @Override 23 | public Shape shape() { 24 | //TODO rotated rectangle? 25 | return new Rectangle(graphics.model.width() * graphics.scaleX, graphics.model.height() * graphics.scaleY); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/RotationAnimation.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.ui.*; 4 | import ritzow.sandbox.util.Utility; 5 | 6 | public class RotationAnimation implements GuiElement { 7 | private final GuiElement child; 8 | private final double radiansPerNano; 9 | private float rotation; 10 | 11 | public RotationAnimation(GuiElement child, double radiansPerNano) { 12 | this.child = child; 13 | this.radiansPerNano = radiansPerNano; 14 | } 15 | 16 | @Override 17 | public void render(GuiRenderer renderer, long nanos) { 18 | renderer.draw(child, 1, 0, 0, 1, 1, rotation); 19 | rotation += nanos * radiansPerNano; 20 | } 21 | 22 | @Override 23 | public Shape shape() { 24 | //account for any orientation of child element (could optimize this further for certain shapes) 25 | Rectangle shape = child.shape().toRectangle(); 26 | return new Circle(Utility.distance(0, 0, shape.width()/2, shape.height()/2)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/Scaler.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.ui.GuiElement; 4 | import ritzow.sandbox.client.ui.GuiRenderer; 5 | import ritzow.sandbox.client.ui.Shape; 6 | 7 | public record Scaler(GuiElement element, float scale) implements GuiElement { 8 | @Override 9 | public void render(GuiRenderer renderer, long nanos) { 10 | renderer.draw(element, 1, 0, 0, scale, scale, 0); 11 | } 12 | 13 | @Override 14 | public Shape shape() { 15 | return element.shape().scale(scale); //TODO multiply by scale? 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/Text.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import java.util.Objects; 4 | import java.util.PrimitiveIterator; 5 | import ritzow.sandbox.client.graphics.GameModels; 6 | import ritzow.sandbox.client.graphics.Model; 7 | import ritzow.sandbox.client.ui.*; 8 | import ritzow.sandbox.client.util.ClientUtility; 9 | 10 | public class Text implements GuiElement { 11 | private static final float SIZE_SCALE = 0.002f; 12 | private final CharPos[] text; 13 | private final Rectangle bounds; 14 | 15 | private static record CharPos(Model model, float x, float scale) {} 16 | 17 | private static final Model MISSING_CHAR_MODEL = GameModels.MODEL_RED_SQUARE; 18 | 19 | /** 20 | * 21 | * @param text The text to display 22 | * @param font The character models usable with the provided GuiRenderer to display 23 | * @param size Font size, in points 24 | * @param spacing The distance between characters //The fraction of a character width to put between each character 25 | * TODO add left justify/centering to constructor and new lines 26 | */ 27 | public Text(CharSequence text, Font font, int size, float spacing) { 28 | //compute max height 29 | float maxHeight = 0; 30 | { 31 | PrimitiveIterator.OfInt scan = text.codePoints().iterator(); 32 | while(scan.hasNext()) { 33 | Model model = font.getModel(scan.nextInt()); 34 | if(model != null && model.height() > maxHeight) maxHeight = model.height(); 35 | } 36 | } 37 | float missingScale = ClientUtility.scaleToHeight(maxHeight, MISSING_CHAR_MODEL); 38 | 39 | //Compute total width 40 | int length = 0; 41 | float glyphWidths = 0; 42 | PrimitiveIterator.OfInt scan = text.codePoints().iterator(); 43 | while(scan.hasNext()) { 44 | Model model = Objects.requireNonNullElse(font.getModel(scan.nextInt()), MISSING_CHAR_MODEL); 45 | glyphWidths += model.width() * (model == MISSING_CHAR_MODEL ? missingScale : 1); 46 | length++; 47 | } 48 | float scale = size * SIZE_SCALE; 49 | float totalWidth = glyphWidths * scale + Math.max(length - 1, 0) * spacing; 50 | this.bounds = new Rectangle(totalWidth, maxHeight * scale); 51 | this.text = new CharPos[length]; 52 | 53 | //Compute positions 54 | int index = 0; 55 | float pos = -totalWidth/2f; 56 | PrimitiveIterator.OfInt iterator = text.codePoints().iterator(); 57 | while(iterator.hasNext()) { 58 | Model model = Objects.requireNonNullElse(font.getModel(iterator.nextInt()), MISSING_CHAR_MODEL); 59 | float charScale = (model == MISSING_CHAR_MODEL ? missingScale * scale : scale); 60 | float width = model.width() * charScale; 61 | pos += width/2f; 62 | this.text[index] = new CharPos(model, pos, charScale); 63 | pos += width/2f + spacing; 64 | index++; 65 | } 66 | } 67 | 68 | @Override 69 | public void render(GuiRenderer renderer, long nanos) { 70 | for(CharPos data : text) { 71 | renderer.draw(data.model, 1.0f, data.x, 0, data.scale, data.scale, 0); 72 | } 73 | } 74 | 75 | @Override 76 | public Shape shape() { 77 | return bounds; 78 | } 79 | 80 | @Override 81 | public Shape shape(Rectangle parent) { 82 | return bounds; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/TextField.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.input.Control; 4 | import ritzow.sandbox.client.ui.Font; 5 | import ritzow.sandbox.client.ui.GuiRenderer; 6 | 7 | public class TextField extends EditableText { 8 | private int cursorIndex; 9 | 10 | public TextField(Font font, float size, float spacing) { 11 | super(font, size, spacing); 12 | cursorIndex = -1; 13 | } 14 | 15 | @Override 16 | public void render(GuiRenderer renderer, long nanos) { 17 | super.render(renderer, nanos); 18 | if(renderer.controls().isNewlyPressed(Control.UI_ACTIVATE) && renderer.mousePos().isPresent()) { 19 | //compute cursorIndex from mouse position and width/height 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/VBox.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import ritzow.sandbox.client.ui.GuiElement; 4 | import ritzow.sandbox.client.ui.GuiRenderer; 5 | import ritzow.sandbox.client.ui.Rectangle; 6 | import ritzow.sandbox.client.ui.Shape; 7 | 8 | //TODO VBox and HBox should check if children have changed scale and recompute bounds 9 | public class VBox implements GuiElement { 10 | private final Alignment[] elements; 11 | private final Rectangle bounds; 12 | 13 | private static record Alignment(GuiElement e, float y) {} 14 | 15 | public VBox(float gap, GuiElement... elements) { 16 | this.elements = new Alignment[elements.length]; 17 | float height = (elements.length - 1) * gap, maxWidth = 0; 18 | for(GuiElement e : elements) { 19 | Rectangle rect = e.shape().toRectangle(); 20 | height += rect.height(); 21 | if(rect.width() > maxWidth) { 22 | maxWidth = rect.width(); 23 | } 24 | } 25 | 26 | float posY = height/2; 27 | for(int i = 0; i < elements.length; i++) { 28 | GuiElement e = elements[i]; 29 | Rectangle bounds = e.shape().toRectangle(); 30 | posY -= bounds.height()/2; 31 | this.elements[i] = new Alignment(e, posY); 32 | posY -= bounds.height()/2 + gap; 33 | } 34 | 35 | this.bounds = new Rectangle(maxWidth, height); 36 | } 37 | 38 | @Override 39 | public void render(GuiRenderer renderer, long nanos) { 40 | for(Alignment e : elements) { 41 | renderer.draw(e.e, 0, e.y()); 42 | } 43 | } 44 | 45 | @Override 46 | public Shape shape() { 47 | return bounds; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/ui/element/VBoxDynamic.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.ui.element; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import ritzow.sandbox.client.ui.GuiElement; 7 | import ritzow.sandbox.client.ui.GuiRenderer; 8 | import ritzow.sandbox.client.ui.Rectangle; 9 | 10 | public class VBoxDynamic implements GuiElement { 11 | private final List elements; 12 | private final float gap; 13 | private final Alignment alignment; 14 | 15 | public enum Alignment { 16 | LEFT { 17 | @Override 18 | float aligned(float maxWidth, float width) { 19 | return width/2f - maxWidth/2f; 20 | } 21 | }, 22 | CENTER { 23 | @Override 24 | float aligned(float maxWidth, float width) { 25 | return 0; 26 | } 27 | }, 28 | RIGHT { 29 | @Override 30 | float aligned(float maxWidth, float width) { 31 | return maxWidth/2f - width/2f; 32 | } 33 | }; 34 | 35 | abstract float aligned(float maxWidth, float width); 36 | } 37 | 38 | public VBoxDynamic(float gap, Alignment alignment, GuiElement... elements) { 39 | this.elements = new ArrayList<>(Arrays.asList(elements)); 40 | this.gap = gap; 41 | this.alignment = alignment; 42 | } 43 | 44 | @Override 45 | public void render(GuiRenderer renderer, long nanos) { 46 | Rectangle bounds = shape(); 47 | float pos = bounds.height()/2f; 48 | for(GuiElement e : elements) { 49 | Rectangle elementBounds = e.shape().toRectangle(); 50 | pos -= elementBounds.height() / 2; 51 | renderer.draw(e, alignment.aligned(bounds.width(), elementBounds.width()), pos); 52 | pos -= elementBounds.height() / 2 + gap; 53 | } 54 | } 55 | 56 | public boolean add(GuiElement element) { 57 | return elements.add(element); 58 | } 59 | 60 | public boolean remove(GuiElement element) { 61 | return elements.remove(element); 62 | } 63 | 64 | @Override 65 | public Rectangle shape() { 66 | float height = (elements.size() - 1) * gap, maxWidth = 0; 67 | for(GuiElement e : elements) { 68 | Rectangle rect = e.shape().toRectangle(); 69 | height += rect.height(); 70 | if(rect.width() > maxWidth) { 71 | maxWidth = rect.width(); 72 | } 73 | } 74 | return new Rectangle(maxWidth, height); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/util/SerializationProvider.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.util; 2 | 3 | import ritzow.sandbox.client.world.block.ClientGlassBlock; 4 | import ritzow.sandbox.client.world.block.ClientDirtBlock; 5 | import ritzow.sandbox.client.world.block.ClientGrassBlock; 6 | import ritzow.sandbox.client.world.entity.ClientItemEntity; 7 | import ritzow.sandbox.client.world.entity.ClientPlayerEntity; 8 | import ritzow.sandbox.client.world.item.ClientBlockItem; 9 | import ritzow.sandbox.data.SerializerReaderWriter; 10 | import ritzow.sandbox.network.Protocol; 11 | import ritzow.sandbox.world.BlockGrid; 12 | import ritzow.sandbox.world.World; 13 | import ritzow.sandbox.world.component.Inventory; 14 | 15 | public final class SerializationProvider { 16 | private static final SerializerReaderWriter provider = new SerializerReaderWriter() 17 | .registerRead(Protocol.DATA_BLOCK_GRID, BlockGrid::new) 18 | .registerRead(Protocol.DATA_WORLD, World::new) 19 | .registerRead(Protocol.DATA_BLOCK_ITEM, ClientBlockItem::new) 20 | .registerRead(Protocol.DATA_DIRT_BLOCK, ClientDirtBlock::getSingleton) 21 | .registerRead(Protocol.DATA_GRASS_BLOCK, ClientGrassBlock::getSingleton) 22 | .registerRead(Protocol.DATA_GLASS_BLOCK, ClientGlassBlock::getSingleton) 23 | .registerRead(Protocol.DATA_PLAYER_ENTITY, ClientPlayerEntity::new) 24 | .registerRead(Protocol.DATA_INVENTORY, Inventory::new) 25 | .registerRead(Protocol.DATA_ITEM_ENTITY, ClientItemEntity::new); 26 | 27 | public static SerializerReaderWriter getProvider() { 28 | return provider; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/block/ClientBlockProperties.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.block; 2 | 3 | import ritzow.sandbox.client.audio.AudioSystem; 4 | import ritzow.sandbox.client.audio.Sound.StandardSound; 5 | import ritzow.sandbox.client.graphics.Camera; 6 | import ritzow.sandbox.client.graphics.Light; 7 | import ritzow.sandbox.client.graphics.Model; 8 | import ritzow.sandbox.world.BlockGrid; 9 | import ritzow.sandbox.world.World; 10 | import ritzow.sandbox.world.block.Block; 11 | 12 | public interface ClientBlockProperties extends Block { 13 | Model getModel(); 14 | boolean isTransparent(); 15 | 16 | default short transparency() { //TODO something like this? 17 | return 255; 18 | } 19 | 20 | default Light lighting() { 21 | return null; 22 | } 23 | 24 | @Override 25 | default void onBreak(World world, float x, float y) { 26 | throw new UnsupportedOperationException("client does not support default onBreak"); 27 | } 28 | 29 | @Override 30 | default void onPlace(World world, float x, float y) { 31 | throw new UnsupportedOperationException("client does not support default onPlace"); 32 | } 33 | 34 | /** 35 | * Should be called when a block is broken in the world. 36 | * @param world the world that the {@code grid} belongs to. 37 | * @param grid the {@link BlockGrid} that the block belongs to. 38 | * @param camera the player's camera. 39 | * @param x the world x position the block was at. 40 | * @param y the world y position the block was at. 41 | */ 42 | default void onBreak(World world, BlockGrid grid, Camera camera, float x, float y) { 43 | AudioSystem.getDefault() 44 | .playSound(StandardSound.BLOCK_BREAK, x, y, 0, 0, 45 | camera.getZoom() * 2 * world.random().nextFloat(0.75f, 1.5f), world.random().nextFloat(0.75f, 1.5f)); 46 | } 47 | 48 | /** 49 | * Should be called when a block is placed in the world. 50 | * @param world the world that the {@code grid} belongs to. 51 | * @param grid the {@link BlockGrid} that the block belongs to. 52 | * @param camera the player's camera. 53 | * @param x the world x position the block was placed at. 54 | * @param y the world y position the block was placed at. 55 | */ 56 | default void onPlace(World world, BlockGrid grid, Camera camera, float x, float y) { 57 | AudioSystem.getDefault() 58 | .playSound(StandardSound.BLOCK_PLACE, x, y, 0, 0, camera.getZoom() * 2, world.random().nextFloat(0.9f, 1.1f)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/block/ClientDirtBlock.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.block; 2 | 3 | import ritzow.sandbox.client.graphics.GameModels; 4 | import ritzow.sandbox.client.graphics.Model; 5 | import ritzow.sandbox.data.TransportableDataReader; 6 | import ritzow.sandbox.world.block.DirtBlock; 7 | 8 | public class ClientDirtBlock extends DirtBlock implements ClientBlockProperties { 9 | 10 | public static final ClientDirtBlock SINGLETON = new ClientDirtBlock(); 11 | 12 | public static ClientDirtBlock getSingleton(@SuppressWarnings("unused") TransportableDataReader reader) { 13 | return SINGLETON; 14 | } 15 | 16 | public ClientDirtBlock() {} 17 | 18 | @Override 19 | public Model getModel() { 20 | return GameModels.MODEL_DIRT_BLOCK; 21 | } 22 | 23 | @Override 24 | public boolean isTransparent() { 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/block/ClientGlassBlock.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.block; 2 | 3 | import ritzow.sandbox.client.graphics.GameModels; 4 | import ritzow.sandbox.client.graphics.Light; 5 | import ritzow.sandbox.client.graphics.Model; 6 | import ritzow.sandbox.client.graphics.StaticLight; 7 | import ritzow.sandbox.data.TransportableDataReader; 8 | import ritzow.sandbox.world.block.GlassBlock; 9 | 10 | public class ClientGlassBlock extends GlassBlock implements ClientBlockProperties { 11 | private static final ClientGlassBlock INSTANCE = new ClientGlassBlock(); 12 | private static final Light LIGHTING = new StaticLight(0, 0, 1, 1, 1, 1); //TODO the posX and posY here need to be used as offsets when adding this to the lights list 13 | 14 | public static ClientGlassBlock getSingleton(TransportableDataReader transportableDataReader) { 15 | return INSTANCE; 16 | } 17 | 18 | @Override 19 | public Model getModel() { 20 | return GameModels.MODEL_GLASS_BLOCK; 21 | } 22 | 23 | @Override 24 | public boolean isTransparent() { 25 | return true; 26 | } 27 | 28 | @Override 29 | public Light lighting() { 30 | return LIGHTING; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/block/ClientGrassBlock.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.block; 2 | 3 | import ritzow.sandbox.client.graphics.GameModels; 4 | import ritzow.sandbox.client.graphics.Model; 5 | import ritzow.sandbox.data.TransportableDataReader; 6 | import ritzow.sandbox.world.block.GrassBlock; 7 | 8 | public class ClientGrassBlock extends GrassBlock implements ClientBlockProperties { 9 | 10 | public static final ClientGrassBlock SINGLETON = new ClientGrassBlock(); 11 | 12 | public static ClientGrassBlock getSingleton(@SuppressWarnings("unused") TransportableDataReader reader) { 13 | return SINGLETON; 14 | } 15 | 16 | @Override 17 | public Model getModel() { 18 | return GameModels.MODEL_GRASS_BLOCK; 19 | } 20 | 21 | @Override 22 | public boolean isTransparent() { 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/entity/ClientItemEntity.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.entity; 2 | 3 | import java.util.random.RandomGeneratorFactory; 4 | import ritzow.sandbox.client.graphics.Graphics; 5 | import ritzow.sandbox.client.graphics.ModelRenderer; 6 | import ritzow.sandbox.client.graphics.Renderable; 7 | import ritzow.sandbox.data.TransportableDataReader; 8 | import ritzow.sandbox.util.Utility; 9 | import ritzow.sandbox.world.World; 10 | import ritzow.sandbox.world.entity.ItemEntity; 11 | import ritzow.sandbox.world.item.Item; 12 | 13 | public final class ClientItemEntity extends ItemEntity implements Renderable { 14 | private final float rotationVelocity; 15 | private float rotation; 16 | 17 | public ClientItemEntity(TransportableDataReader data) { 18 | super(data); 19 | this.rotationVelocity = Utility.convertPerSecondToPerNano( 20 | (float)RandomGeneratorFactory.getDefault().create().nextDouble(-Math.PI, Math.PI)); 21 | } 22 | 23 | @Override 24 | public void update(World world, long ns) { 25 | super.update(world, ns); 26 | this.rotation = Math.fma(ns, rotationVelocity, this.rotation); 27 | } 28 | 29 | @Override 30 | public void render(ModelRenderer renderer, float exposure) { 31 | Graphics g = ((Graphics)item); 32 | renderer.queueRender( 33 | g.getModel(), 34 | g.getOpacity(), 35 | exposure, 36 | positionX, 37 | positionY, 38 | g.getScaleX() * 0.5f, 39 | g.getScaleY() * 0.5f, 40 | g.getRotation() + rotation 41 | ); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return super.toString() + ", item = (" + item + ")"; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/entity/ClientPlayerEntity.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.entity; 2 | 3 | import java.util.List; 4 | import ritzow.sandbox.client.graphics.*; 5 | import ritzow.sandbox.data.TransportableDataReader; 6 | import ritzow.sandbox.world.entity.PlayerEntity; 7 | 8 | /** 9 | * Represents a player 10 | * @author Solomon Ritzow 11 | */ 12 | public class ClientPlayerEntity extends PlayerEntity implements Renderable, Lit { 13 | public ClientPlayerEntity(int entityID) { 14 | super(entityID); 15 | } 16 | 17 | public ClientPlayerEntity(TransportableDataReader input) { 18 | super(input); 19 | } 20 | 21 | @Override 22 | public void render(ModelRenderer renderer, float exposure) { 23 | renderer.queueRender( 24 | GameModels.MODEL_GREEN_FACE, 25 | 1.0f, 26 | exposure, 27 | positionX, 28 | positionY + (down ? 0 : SIZE_SCALE / 2), 29 | SIZE_SCALE, 30 | SIZE_SCALE, 31 | 0.0f 32 | ); 33 | if(!down) { 34 | renderer.queueRender( 35 | GameModels.MODEL_BLUE_SQUARE, 36 | 1.0f, 37 | exposure, 38 | positionX, 39 | positionY - SIZE_SCALE / 2, 40 | SIZE_SCALE, 41 | SIZE_SCALE, 42 | 0 43 | ); 44 | } 45 | } 46 | 47 | private static final List LIGHTS_STANDING = List.of( 48 | new StaticLight(0, 0.5f, 0, 1, 0, 20), 49 | new StaticLight(0, -0.5f, 0, 0, 1, 20) 50 | ); 51 | 52 | private static final List LIGHTS_CROUCHED = List.of( 53 | new StaticLight(0, 0, 1, 1, 1, 20) 54 | ); 55 | 56 | 57 | @Override 58 | public Iterable lights() { 59 | return down ? LIGHTS_CROUCHED : LIGHTS_STANDING; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/src/ritzow/sandbox/client/world/item/ClientBlockItem.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.world.item; 2 | 3 | import ritzow.sandbox.client.graphics.Graphics; 4 | import ritzow.sandbox.client.graphics.Model; 5 | import ritzow.sandbox.client.world.block.ClientBlockProperties; 6 | import ritzow.sandbox.data.TransportableDataReader; 7 | import ritzow.sandbox.world.block.Block; 8 | import ritzow.sandbox.world.item.BlockItem; 9 | 10 | public final class ClientBlockItem extends BlockItem implements Graphics { 11 | 12 | public ClientBlockItem(TransportableDataReader input) { 13 | super(input); 14 | } 15 | 16 | public ClientBlockItem(Block block) { 17 | super(block); 18 | } 19 | 20 | @Override 21 | public ClientBlockProperties getBlock() { 22 | return (ClientBlockProperties)block; 23 | } 24 | 25 | @Override 26 | public Model getModel() { 27 | return ((ClientBlockProperties)block).getModel(); 28 | } 29 | 30 | @Override 31 | public float getOpacity() { 32 | return 1; 33 | } 34 | 35 | @Override 36 | public float getScaleX() { 37 | return 1; 38 | } 39 | 40 | @Override 41 | public float getScaleY() { 42 | return 1; 43 | } 44 | 45 | @Override 46 | public float getRotation() { 47 | return 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/test/ritzow/sandbox/client/test/RunTest.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.client.test; 2 | 3 | import java.io.IOException; 4 | import org.junit.jupiter.api.Test; 5 | import ritzow.sandbox.client.StartClient; 6 | 7 | public class RunTest { 8 | @Test 9 | void run() throws IOException { 10 | StartClient.main(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /clientlauncher/Linux/Sandbox2DLinuxLauncher.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | ARM 7 | 8 | 9 | Release 10 | ARM 11 | 12 | 13 | Debug 14 | ARM64 15 | 16 | 17 | Release 18 | ARM64 19 | 20 | 21 | Debug 22 | x86 23 | 24 | 25 | Release 26 | x86 27 | 28 | 29 | Debug 30 | x64 31 | 32 | 33 | Release 34 | x64 35 | 36 | 37 | 38 | {5f10a7a1-93a7-4374-a8f2-cf97113a6aa9} 39 | Linux 40 | Sandbox2DLinuxLauncher 41 | 15.0 42 | Linux 43 | 1.0 44 | Generic 45 | {D51BCBC9-82E9-4017-911E-C93873C4EA2B} 46 | 47 | 48 | 49 | true 50 | 51 | 52 | false 53 | 54 | 55 | true 56 | 57 | 58 | false 59 | 60 | 61 | true 62 | 63 | 64 | false 65 | 66 | 67 | false 68 | 69 | 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /clientlauncher/Linux/Sandbox2DLinuxLauncher/Sandbox2DLinuxLauncher.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | ARM 7 | 8 | 9 | Release 10 | ARM 11 | 12 | 13 | Debug 14 | ARM64 15 | 16 | 17 | Release 18 | ARM64 19 | 20 | 21 | Debug 22 | x86 23 | 24 | 25 | Release 26 | x86 27 | 28 | 29 | Debug 30 | x64 31 | 32 | 33 | Release 34 | x64 35 | 36 | 37 | StaticRelease 38 | ARM64 39 | 40 | 41 | 42 | {5f10a7a1-93a7-4374-a8f2-cf97113a6aa9} 43 | Linux 44 | Sandbox2DLinuxLauncher 45 | 15.0 46 | Linux 47 | 1.0 48 | Generic 49 | {D51BCBC9-82E9-4017-911E-C93873C4EA2B} 50 | Sandbox2DLauncherLinux 51 | 52 | 53 | 54 | true 55 | 56 | 57 | false 58 | 59 | 60 | true 61 | 62 | 63 | false 64 | 65 | 66 | true 67 | 68 | 69 | false 70 | 71 | 72 | false 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /clientlauncher/Linux/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | printf("hello from Sandbox2DLinuxLauncher!\n"); 6 | return 0; 7 | } -------------------------------------------------------------------------------- /clientlauncher/Shared/shared.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static JavaVMOption JVM_OPTIONS[] = { 5 | {(char*)"--enable-preview"}, 6 | {(char*)"-Djdk.module.main=ritzow.sandbox.client"}, 7 | {(char*)"-XX:-UsePerfData"}, 8 | {(char*)"-Dorg.lwjgl.util.NoChecks=true"}, 9 | {(char*)"-Dorg.lwjgl.openal.explicitInit=true"}, 10 | {(char*)"-Dorg.lwjgl.opengl.explicitInit=true"}, 11 | {(char*)"-Dorg.lwjgl.util.NoFunctionChecks=true"} //TODO set these properties using Configuration class instead of system properties. 12 | }; 13 | 14 | constexpr JavaVMInitArgs GetJavaInitArgs() noexcept { 15 | return { 16 | .version = JNI_VERSION_10, 17 | .nOptions = sizeof(JVM_OPTIONS) / sizeof(JavaVMOption), 18 | .options = JVM_OPTIONS, 19 | .ignoreUnrecognized = false 20 | }; 21 | } 22 | 23 | constexpr const wchar_t* GetErrorString(jint result) noexcept { 24 | switch (result) { 25 | case JNI_OK: return L"Success"; 26 | case JNI_ERR: return L"Unknown error"; 27 | case JNI_EDETACHED: return L"Thread detached from the VM"; 28 | case JNI_EVERSION: return L"JNI version error"; 29 | case JNI_ENOMEM: return L"Not enough memory"; 30 | case JNI_EEXIST: return L"VM already created"; 31 | case JNI_EINVAL: return L"Invalid arguments"; 32 | default: return L"Unknown error"; 33 | } 34 | } 35 | 36 | inline void DisplayError(const wchar_t* title, const wchar_t* message); 37 | 38 | const wchar_t* GetExceptionString(JNIEnv* env) noexcept { 39 | jthrowable exception = env->ExceptionOccurred(); 40 | jclass throwableClass = env->FindClass("java/lang/Throwable"); 41 | jclass elementClass = env->FindClass("java/lang/StackTraceElement"); 42 | 43 | jmethodID throwableToString = env->GetMethodID(throwableClass, 44 | "toString", "()Ljava/lang/String;"); 45 | jmethodID getStackTrace = env->GetMethodID(throwableClass, 46 | "getStackTrace", "()[Ljava/lang/StackTraceElement;"); 47 | jmethodID elementToString = env->GetMethodID(elementClass, 48 | "toString", "()Ljava/lang/String;"); 49 | env->DeleteLocalRef(throwableClass); 50 | env->DeleteLocalRef(elementClass); 51 | 52 | jstring msgString = (jstring)env->CallObjectMethod(exception, throwableToString); 53 | jobjectArray trace = (jobjectArray)env->CallObjectMethod(exception, getStackTrace); 54 | const char* message = env->GetStringUTFChars(msgString, nullptr); 55 | std::wstringstream builder; 56 | builder << message << '\n'; 57 | env->ReleaseStringUTFChars(msgString, message); 58 | env->DeleteLocalRef(msgString); 59 | jsize length = env->GetArrayLength(trace); 60 | for (int i = 0; i < length; i++) { 61 | jobject element = env->GetObjectArrayElement(trace, i); 62 | jstring msg = static_cast(env->CallObjectMethod(element, elementToString)); 63 | const jchar* str = env->GetStringChars(msg, nullptr); 64 | builder << L" at " << (const wchar_t*)str << '\n'; 65 | env->ReleaseStringChars(msg, str); 66 | env->DeleteLocalRef(msg); 67 | } 68 | env->DeleteLocalRef(trace); 69 | return builder.str().c_str(); 70 | } 71 | 72 | void RunGame(JNIEnv* env, int argc, wchar_t** args) { 73 | jclass classMain = env->FindClass("ritzow/sandbox/client/StartClient"); 74 | jclass classString = env->FindClass("java/lang/String"); 75 | jmethodID methodMain = env->GetStaticMethodID(classMain, "main", "([Ljava/lang/String;)V"); 76 | jobjectArray jargs = env->NewObjectArray(argc, classString, nullptr); 77 | env->DeleteLocalRef(classString); 78 | for (int i = 0; i < argc; i++) { 79 | jstring jarg = env->NewString((const jchar*)args[i], (jsize)wcslen(args[i])); 80 | env->SetObjectArrayElement(jargs, i, jarg); 81 | env->DeleteLocalRef(jarg); 82 | } 83 | env->CallStaticVoidMethod(classMain, methodMain, jargs); 84 | env->DeleteLocalRef(jargs); 85 | env->DeleteLocalRef(classMain); 86 | if (env->ExceptionCheck()) 87 | DisplayError(L"Java Exception Occurred on Main Thread", GetExceptionString(env)); 88 | } 89 | 90 | typedef jint(JNICALL* StartJVM)(JavaVM**, JNIEnv**, JavaVMInitArgs*); -------------------------------------------------------------------------------- /clientlauncher/Windows/launch.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | inline const bool SHOW_CONSOLE = true; 8 | 9 | inline void DisplayError(const wchar_t* title, const wchar_t* message) { 10 | MessageBoxW(nullptr, message, title, MB_OK | MB_ICONERROR); 11 | } 12 | 13 | void SetWorkingDirectory(HMODULE mod) { 14 | WCHAR program_path[MAX_PATH]; 15 | DWORD length = GetModuleFileNameW(mod, program_path, MAX_PATH); 16 | PathCchRemoveFileSpec(program_path, length); 17 | SetCurrentDirectoryW(program_path); 18 | } 19 | 20 | //void SetWorkingDirectory(HMODULE module) { 21 | // WCHAR program_path[MAX_PATH]; 22 | // DWORD nSize = MAX_PATH; 23 | // DWORD length; 24 | // do { 25 | // length = GetModuleFileNameW(module, program_path, MAX_PATH); 26 | // if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { 27 | // 28 | // } 29 | // } while (GetLastError() == ERROR_INSUFFICIENT_BUFFER); 30 | // PathCchRemoveFileSpec(program_path, length); 31 | // SetCurrentDirectoryW(program_path); 32 | //} 33 | 34 | void SetupConsole(LPWSTR args) { 35 | AllocConsole(); 36 | FILE* file; 37 | _wfreopen_s(&file, L"CONOUT$", L"w", stdout); 38 | if (wcslen(args) > 0) 39 | std::wcout << "Program Arguments: \"" << args << '"' << std::endl; 40 | } 41 | 42 | INT WINAPI wWinMain(_In_ HMODULE mod, _In_opt_ HINSTANCE, _In_ LPWSTR lpCmdLine, _In_ INT) { 43 | if constexpr (SHOW_CONSOLE) SetupConsole(lpCmdLine); 44 | SetWorkingDirectory(mod); 45 | HMODULE dll = LoadLibraryW(LR"(jvm\bin\server\jvm.dll)"); 46 | if (dll != nullptr) { 47 | //TODO create another file with wWinMain that uses static linking by simply calling JNI_CreateJavaVM() without dll usage 48 | JavaVM* vm; JNIEnv* env; JavaVMInitArgs vmargs = GetJavaInitArgs(); 49 | jint result = ((StartJVM)GetProcAddress(dll, "JNI_CreateJavaVM"))(&vm, &env, &vmargs); 50 | if (result == JNI_OK) { 51 | RunGame(env, __argc - 1, __wargv + 1); 52 | vm->DestroyJavaVM(); 53 | FreeLibrary(dll); 54 | return EXIT_SUCCESS; 55 | } else { 56 | std::wstring message = L"Could not create JVM: "; 57 | message += GetErrorString(result); 58 | DisplayError(L"Java Virtual Machine Creation Failed", message.c_str()); 59 | FreeLibrary(dll); 60 | return EXIT_FAILURE; 61 | } 62 | } else { 63 | DisplayError(L"Java Libray Load Failed", L"Failed to load jvm.dll"); 64 | return EXIT_FAILURE; 65 | } 66 | } -------------------------------------------------------------------------------- /clientlauncher/Windows/launch_static.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | inline const bool SHOW_CONSOLE = false; 8 | 9 | inline void DisplayError(const wchar_t* title, const wchar_t* message) { 10 | MessageBoxW(nullptr, message, title, MB_OK | MB_ICONERROR); 11 | } 12 | 13 | void SetWorkingDirectory(HMODULE mod) { 14 | WCHAR program_path[MAX_PATH]; 15 | DWORD length = GetModuleFileNameW(mod, program_path, MAX_PATH); 16 | PathCchRemoveFileSpec(program_path, length); 17 | SetCurrentDirectoryW(program_path); 18 | } 19 | 20 | //void SetWorkingDirectory(HMODULE module) { 21 | // WCHAR program_path[MAX_PATH]; 22 | // DWORD nSize = MAX_PATH; 23 | // DWORD length; 24 | // do { 25 | // length = GetModuleFileNameW(module, program_path, MAX_PATH); 26 | // if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { 27 | // 28 | // } 29 | // } while (GetLastError() == ERROR_INSUFFICIENT_BUFFER); 30 | // PathCchRemoveFileSpec(program_path, length); 31 | // SetCurrentDirectoryW(program_path); 32 | //} 33 | 34 | void SetupConsole(LPWSTR args) { 35 | AllocConsole(); 36 | FILE* file; 37 | _wfreopen_s(&file, L"CONOUT$", L"w", stdout); 38 | if (wcslen(args) > 0) 39 | std::wcout << "Program Arguments: \"" << args << '"' << std::endl; 40 | } 41 | 42 | INT WINAPI wWinMain(_In_ HMODULE mod, _In_opt_ HINSTANCE, _In_ LPWSTR lpCmdLine, _In_ INT) { 43 | if constexpr (SHOW_CONSOLE) SetupConsole(lpCmdLine); 44 | SetWorkingDirectory(mod); 45 | JavaVM* vm; 46 | JNIEnv* env; 47 | JavaVMInitArgs vmargs = GetJavaInitArgs(); 48 | jint result = JNI_CreateJavaVM(&vm, (void**) &env, &vmargs); 49 | if (result == JNI_OK) { 50 | RunGame(env, __argc - 1, __wargv + 1); 51 | vm->DestroyJavaVM(); 52 | return EXIT_SUCCESS; 53 | } 54 | else { 55 | std::wstring message = L"Could not create JVM: "; 56 | message += GetErrorString(result); 57 | DisplayError(L"Java Virtual Machine Creation Failed", message.c_str()); 58 | return EXIT_FAILURE; 59 | } 60 | } -------------------------------------------------------------------------------- /clientlauncher/Windows/resources/greenFace.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/clientlauncher/Windows/resources/greenFace.ico -------------------------------------------------------------------------------- /clientlauncher/Windows/resources/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by resource.rc 4 | // 5 | #define IDI_ICON1 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 105 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /clientlauncher/Windows/resources/resource.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/clientlauncher/Windows/resources/resource.rc -------------------------------------------------------------------------------- /clientlauncher/build.ps1: -------------------------------------------------------------------------------- 1 | Write-Output "Compiling Build.java." 2 | java --enable-preview --source 16 "Shared\src\ritzow\sandbox\build\Build.java" -------------------------------------------------------------------------------- /clientlauncher/macOS/prepare-build.bat: -------------------------------------------------------------------------------- 1 | ::macOS build 2 | @echo off 3 | 4 | set "output=x64\Release\Output" 5 | 6 | ::clean up previous version 7 | @echo Removing previous build files 8 | rmdir /S /Q "include" 9 | rmdir /S /Q "%output%" 10 | rmdir /S /Q "x64\Development\Output" 11 | mkdir "%output%" 12 | 13 | set "client=..\..\client" 14 | set "shared=..\..\shared" 15 | set "lwjgl=%client%\libraries\lwjgl" 16 | 17 | ::create the java runtime TODO call Build.java 18 | ::and move the resulting files instead of relying on 19 | ::Eclipse IDE build for client/shared modules 20 | @echo Running jlink 21 | "C:\Program Files\Java\jdk-12\bin\jlink.exe" ^ 22 | --compress=2 ^ 23 | --no-man-pages ^ 24 | --strip-debug ^ 25 | --endian little ^ 26 | --module-path "lib\jmods;%client%\bin;%shared%\bin;%lwjgl%\lwjgl.jar;%lwjgl%\lwjgl-glfw.jar;%lwjgl%\lwjgl-opengl.jar;%lwjgl%\lwjgl-openal.jar;" ^ 27 | --add-modules java.base,jdk.unsupported,ritzow.sandbox.client,ritzow.sandbox.shared,org.lwjgl,org.lwjgl.glfw,org.lwjgl.opengl,org.lwjgl.openal ^ 28 | --output "%output%\jvm" 29 | 30 | ::delete unecessary files kept by jlink 31 | @echo Deleting unnecessary files 32 | del "%output%\jvm\bin\keytool" 33 | 34 | ::copy files required to compile 35 | @echo Moving header files 36 | move "%output%\jvm\include" "lib\include" 37 | copy "lib\include\darwin" "include" 38 | rmdir /S /Q "lib\include\darwin" 39 | 40 | ::copy resources to output 41 | @echo Copying game resources and launcher to output 42 | xcopy /E /Y /Q "%client%\resources" "%output%\resources\" 43 | xcopy "src\run.command" "%output%\" 44 | 45 | ::copy natives 46 | @echo Copying natives libraries 47 | xcopy /E /Y "lib\natives" "%output%\" 48 | 49 | ::create zipped program archive 50 | @echo Write output to archive 51 | 7z a -tzip -mx1 sandbox2d.zip ".\%output%\*" 52 | 53 | ::clang++ -I ..\lib\usr\include -target x86_64-apple-darwin-macho -stdlib="libc++" launch.cpp 54 | 55 | PAUSE 56 | -------------------------------------------------------------------------------- /clientlauncher/macOS/src/launch.cpp: -------------------------------------------------------------------------------- 1 | #include "../lib/usr/include/stdio.h" 2 | #include "../lib/usr/include/stdlib.h" 3 | //#include "../lib/usr/include/wchar.h" 4 | #include "../lib/usr/include/dlfcn.h" 5 | //#include "../../Shared/shared.cpp" 6 | #include "../include/jni.h" 7 | 8 | JavaVMInitArgs GetJavaInitArgs() { 9 | static JavaVMOption options[] = { 10 | {(char*)"-Djdk.module.main=ritzow.sandbox.client"}, 11 | {(char*)"--enable-preview"}, //Java 12 switch expressions 12 | {(char*)"vfprintf", *vfprintf} 13 | }; 14 | 15 | JavaVMInitArgs args; 16 | args.version = JNI_VERSION_10; 17 | args.ignoreUnrecognized = false; 18 | args.nOptions = sizeof(options) / sizeof(JavaVMOption); 19 | args.options = options; 20 | return args; 21 | } 22 | 23 | typedef jint(JNICALL* CreateJavaVM)(JavaVM**, JNIEnv**, JavaVMInitArgs*); 24 | 25 | int wmain(int argc, const wchar_t* argv[]) { 26 | void* dylib = dlopen("jdk/Contents/Home/lib/server/libjvm.dylib", RTLD_LAZY | RTLD_LOCAL); 27 | if (dylib != nullptr) { 28 | JavaVM* vm; 29 | JNIEnv* env; 30 | jint result = ((CreateJavaVM)dlsym(dylib, "JNI_CreateJavaVM")) 31 | (&vm, &env, &GetJavaInitArgs()); 32 | 33 | if (result == JNI_OK) { 34 | /*std::wstringstream builder; 35 | for (int i = 0; i < argc; i++) { 36 | builder << argv[i]; 37 | } 38 | RunGame(env, builder.str().c_str());*/ 39 | 40 | jclass classMain = env->FindClass("ritzow/sandbox/client/StartClient"); 41 | jmethodID methodMain = env->GetStaticMethodID(classMain, "start", "(Ljava/lang/String;)V"); 42 | jstring jargs = env->NewString((const jchar*)L"", (jsize)0); 43 | env->CallStaticVoidMethod(classMain, methodMain, jargs); 44 | env->DeleteLocalRef(classMain); 45 | env->DeleteLocalRef(jargs); 46 | if (env->ExceptionCheck()) env->ExceptionDescribe(); 47 | 48 | dlclose(dylib); 49 | return EXIT_SUCCESS; 50 | } else { 51 | wprintf(L"Could not create JVM: "); 52 | //wprintf(L"%ls", getErrorString(result)); 53 | return EXIT_FAILURE; 54 | } 55 | } else { 56 | wprintf(L"failed to load libjvm.dylib"); 57 | return EXIT_FAILURE; 58 | } 59 | } -------------------------------------------------------------------------------- /clientlauncher/macOS/src/run.command: -------------------------------------------------------------------------------- 1 | cd -- "$(dirname "$0")". 2 | ./jvm/bin/java -XstartOnFirstThread --enable-preview -m ritzow.sandbox.client/ritzow.sandbox.client.StartClient -------------------------------------------------------------------------------- /clientlauncher/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | net.ritzow 6 | sandbox2d-parent 7 | ${revision} 8 | 9 | 10 | sandbox2d-clientlauncher 11 | pom 12 | 13 | 14 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | net.ritzow 4 | sandbox2d-parent 5 | ${revision} 6 | pom 7 | 8 | 9 | 1.0-SNAPSHOT 10 | 19 11 | 19 12 | 19 13 | UTF-8 14 | 15 | 16 | 17 | client 18 | clientlauncher 19 | server 20 | serverlauncher 21 | shared 22 | 23 | 24 | 25 | 26 | 27 | net.ritzow 28 | sandbox2d-shared 29 | ${revision} 30 | 31 | 32 | net.ritzow 33 | sandbox2d-client 34 | ${revision} 35 | 36 | 37 | net.ritzow 38 | sandbox2d-server 39 | ${revision} 40 | 41 | 42 | net.ritzow 43 | sandbox2d-server-launcher 44 | ${revision} 45 | 46 | 47 | net.ritzow 48 | sandbox2d-clientlauncher 49 | ${revision} 50 | 51 | 52 | org.junit.jupiter 53 | junit-jupiter-api 54 | 5.9.1 55 | test 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-surefire-plugin 66 | 3.0.0-M7 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Sandbox2D](https://i.imgur.com/8GxCB4C.png) 2 | 3 | # Sandbox2D 4 | A 2D block building and destruction platformer game written in Java that uses OpenGL for graphics, OpenAL for audio, GLFW for cross-platform windowing and input, and LWJGL for bindings to the afformentioned libraries and APIs. 5 | 6 | # Repository Structure 7 | **/shared**: Contains code shared by client and server *(module ritzow.sandbox.shared)*. 8 | 9 | **/client**: Contains client-side code and resources including game assets and OpenGL shader code *(module ritzow.sandbox.client)*. 10 | 11 | **/clientlauncher**: native launcher and client build script. 12 | 13 | **/server**: The game server *(module ritzow.sandbox.server)*. 14 | 15 | **/serverlauncher**: JavaFX dedicated server GUI *(module ritzow.sandbox.serverlauncher)*. 16 | 17 | # Libraries 18 | ### [LWJGL](https://www.lwjgl.org/customize) 19 | 20 | Provides bindings to GLFW, OpenGL, and OpenAL. 21 | 22 | #### Download Configuration 23 | ```json 24 | { 25 | "build": "stable", 26 | "mode": "zip", 27 | "platform": [ 28 | "windows", 29 | "macos", 30 | "linux" 31 | ], 32 | "javadoc": true, 33 | "source": true, 34 | "contents": [ 35 | "lwjgl", 36 | "lwjgl-glfw", 37 | "lwjgl-opengl", 38 | "lwjgl-openal" 39 | ] 40 | } 41 | ``` 42 | ### TWL's PNGDecoder: 43 | Used for loading textures. Does not seem to be available directly anymore as the official website seems to be in use by scammers. A modified copy is included in the client module (and other libraries including slick-util also include a copy). 44 | 45 | ### [JSON-java](https://github.com/stleary/JSON-java) 46 | 47 | A simple JSON parser reference implementation used for loading GLTF files in the client (not actually implemented). 48 | 49 | ### [JavaFX](https://openjfx.io/) 50 | 51 | Used for the server launcher. Use the latest version. 52 | 53 | ### [Apache Commons Validator](http://commons.apache.org/proper/commons-validator/) 54 | 55 | Used to provide intuitive error display in the server launcher. 56 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | net.ritzow 6 | sandbox2d-parent 7 | ${revision} 8 | 9 | 10 | sandbox2d-server 11 | 12 | 13 | src 14 | test 15 | 16 | 17 | org.apache.maven.plugins 18 | maven-surefire-plugin 19 | 20 | 21 | 22 | 23 | 24 | 25 | net.ritzow 26 | sandbox2d-shared 27 | 28 | 29 | org.junit.jupiter 30 | junit-jupiter-api 31 | test 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /server/src/module-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Solomon Ritzow 3 | * 4 | */ 5 | module ritzow.sandbox.server { 6 | requires ritzow.sandbox.shared; 7 | exports ritzow.sandbox.server.network; 8 | exports ritzow.sandbox.server; 9 | } -------------------------------------------------------------------------------- /server/src/ritzow/sandbox/server/CommandParser.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Queue; 6 | import java.util.Scanner; 7 | import java.util.concurrent.ConcurrentLinkedQueue; 8 | import java.util.function.Consumer; 9 | import java.util.regex.Pattern; 10 | 11 | class CommandParser implements Runnable { 12 | private static final Pattern nameMatcher = Pattern.compile("\\w+"); 13 | private static final CommandEntry UNKNOWN_COMMAND = 14 | new CommandEntry(args -> System.out.println("Unknown command."), false); 15 | 16 | private final Map commands; 17 | private final Queue commandQueue; 18 | 19 | private static final class CommandEntry { 20 | final Consumer action; 21 | final boolean terminateParser; 22 | 23 | private CommandEntry(Consumer action, boolean terminateParser) { 24 | super(); 25 | this.action = action; 26 | this.terminateParser = terminateParser; 27 | } 28 | } 29 | 30 | public CommandParser() { 31 | commands = new HashMap<>(); 32 | commandQueue = new ConcurrentLinkedQueue<>(); 33 | } 34 | 35 | public CommandParser register(String name, Consumer action, boolean terminateParser) { 36 | if(commands.putIfAbsent(name.toLowerCase(), new CommandEntry(action, terminateParser)) != null) 37 | throw new IllegalArgumentException(name + " already registered"); 38 | return this; 39 | } 40 | 41 | @Override 42 | public void run() { 43 | System.out.println("Available Commands: (" + String.join(", ", commands.keySet()) + ")"); 44 | try(var scan = new Scanner(System.in)) { 45 | CommandEntry entry; 46 | do { 47 | entry = scan.hasNext(nameMatcher) ? 48 | commands.getOrDefault(scan.next(nameMatcher).toLowerCase(), UNKNOWN_COMMAND) : 49 | UNKNOWN_COMMAND; 50 | Consumer command = entry.action; 51 | String args = scan.nextLine().strip(); 52 | commandQueue.add(() -> command.accept(args)); 53 | } while(!entry.terminateParser); 54 | } 55 | } 56 | 57 | public void update() { 58 | while(!commandQueue.isEmpty()) { 59 | commandQueue.remove().run(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/ritzow/sandbox/server/SerializationProvider.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server; 2 | 3 | import ritzow.sandbox.data.SerializerReaderWriter; 4 | import ritzow.sandbox.network.Protocol; 5 | import ritzow.sandbox.server.world.entity.ServerPlayerEntity; 6 | import ritzow.sandbox.world.BlockGrid; 7 | import ritzow.sandbox.world.World; 8 | import ritzow.sandbox.world.block.DirtBlock; 9 | import ritzow.sandbox.world.block.GlassBlock; 10 | import ritzow.sandbox.world.block.GrassBlock; 11 | import ritzow.sandbox.world.component.Inventory; 12 | import ritzow.sandbox.world.entity.ItemEntity; 13 | import ritzow.sandbox.world.item.BlockItem; 14 | 15 | public class SerializationProvider { 16 | private static final SerializerReaderWriter provider = new SerializerReaderWriter() 17 | .register(Protocol.DATA_WORLD, World.class, World::new) 18 | .register(Protocol.DATA_BLOCK_GRID, BlockGrid.class, BlockGrid::new) 19 | .register(Protocol.DATA_BLOCK_ITEM, BlockItem.class, BlockItem::new) 20 | .register(Protocol.DATA_DIRT_BLOCK, DirtBlock.class, reader -> DirtBlock.INSTANCE) 21 | .register(Protocol.DATA_GRASS_BLOCK, GrassBlock.class, reader -> GrassBlock.INSTANCE) 22 | .register(Protocol.DATA_GLASS_BLOCK, GlassBlock.class, reader -> GlassBlock.INSTANCE) 23 | .register(Protocol.DATA_ITEM_ENTITY, ItemEntity.class, ItemEntity::new) 24 | .registerWrite(Protocol.DATA_INVENTORY, Inventory.class) 25 | .registerWrite(Protocol.DATA_PLAYER_ENTITY, ServerPlayerEntity.class); 26 | 27 | public static SerializerReaderWriter getProvider() { 28 | return provider; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/ritzow/sandbox/server/network/ClientBadDataException.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server.network; 2 | 3 | public class ClientBadDataException extends RuntimeException { 4 | 5 | public ClientBadDataException() { 6 | super(null, null, false, false); 7 | } 8 | 9 | public ClientBadDataException(String reason) { 10 | super(reason, null, false, false); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/src/ritzow/sandbox/server/network/ClientNetworkInfo.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server.network; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.ArrayDeque; 5 | import java.util.PriorityQueue; 6 | import java.util.Queue; 7 | import ritzow.sandbox.network.ReceivePacket; 8 | import ritzow.sandbox.network.SendPacket; 9 | 10 | public class ClientNetworkInfo { 11 | final InetSocketAddress address; 12 | final Queue sendQueue; 13 | final Queue receiveQueue; 14 | int sendMessageID = 0, lastSendReliableID = -1, headProcessedID = -1; 15 | long lastMessageProcessTime; 16 | 17 | /** Client reliable message round trip time in nanoseconds */ 18 | long ping; 19 | 20 | ClientNetworkInfo(InetSocketAddress address) { 21 | this.address = address; 22 | sendQueue = new ArrayDeque<>(); 23 | receiveQueue = new PriorityQueue<>(); 24 | } 25 | 26 | @Override 27 | public boolean equals(Object obj) { 28 | return obj instanceof ClientNetworkInfo client && address.equals(client.address); 29 | } 30 | 31 | public void send(byte[] data, boolean reliable) { 32 | sendQueue.add(new SendPacket(data, sendMessageID, lastSendReliableID, reliable, -1)); 33 | if(reliable) lastSendReliableID = sendMessageID; 34 | sendMessageID++; 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return super.hashCode(); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "ClientNetworkInfo{" + 45 | "address=" + address + 46 | ", sendQueue=" + sendQueue + 47 | ", receiveQueue=" + receiveQueue + 48 | ", sendMessageID=" + sendMessageID + 49 | ", lastSendReliableID=" + lastSendReliableID + 50 | ", headProcessedID=" + headProcessedID + 51 | ", lastMessageReceiveTime=" + lastMessageProcessTime + 52 | ", ping=" + ping + 53 | '}'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/ritzow/sandbox/server/network/ClientState.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server.network; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.time.Instant; 5 | import java.util.ArrayDeque; 6 | import java.util.Queue; 7 | import ritzow.sandbox.data.Bytes; 8 | import ritzow.sandbox.network.NetworkUtility; 9 | import ritzow.sandbox.server.world.entity.ServerPlayerEntity; 10 | 11 | public class ClientState extends ClientNetworkInfo { 12 | /** after the client acks the connect ack */ 13 | public static final byte 14 | STATUS_CONNECTED = 0, 15 | /** if the client doesn't send an ack within ack interval */ 16 | STATUS_TIMED_OUT = 1, 17 | /** if server kicks a player or shuts down */ 18 | STATUS_KICKED = 2, 19 | /** if the client manually disconnects */ 20 | STATUS_LEAVE = 3, 21 | /** if the server rejects the client */ 22 | STATUS_REJECTED = 4, 23 | /** if the client has received the world and player and notifies server */ 24 | STATUS_IN_GAME = 5, 25 | /** if the client sent data that didn't make sense */ 26 | STATUS_INVALID = 6; 27 | 28 | byte status; 29 | String disconnectReason; 30 | Queue recordedSend; 31 | 32 | /** Last player action times in nanoseconds offset */ 33 | long lastPlayerStateUpdate; 34 | Instant nextUseTime; 35 | ServerPlayerEntity player; 36 | 37 | ClientState(InetSocketAddress address) { 38 | super(address); 39 | status = STATUS_CONNECTED; 40 | recordedSend = new ArrayDeque<>(); 41 | nextUseTime = Instant.EPOCH; 42 | } 43 | 44 | public void sendRecorded() { 45 | while(!recordedSend.isEmpty()) { 46 | var val =recordedSend.poll(); 47 | System.out.println(Bytes.getShort(val, 0)); 48 | send(val, true); 49 | } 50 | recordedSend = null; 51 | } 52 | 53 | static String statusToString(byte status) { 54 | return switch(status) { 55 | case STATUS_CONNECTED -> "connected"; 56 | case STATUS_TIMED_OUT -> "timed out"; 57 | case STATUS_KICKED -> "kicked"; 58 | case STATUS_LEAVE -> "leave"; 59 | case STATUS_REJECTED -> "rejected"; 60 | case STATUS_IN_GAME -> "in-game"; 61 | case STATUS_INVALID -> "invalid"; 62 | default -> "unknown"; 63 | }; 64 | } 65 | 66 | boolean inGame() { 67 | return status == STATUS_IN_GAME; 68 | } 69 | 70 | boolean hasPending() { 71 | return !sendQueue.isEmpty(); 72 | } 73 | 74 | public String formattedName() { 75 | return NetworkUtility.formatAddress(address) + " (" + statusToString(status) + ')'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server/src/ritzow/sandbox/server/world/entity/ServerPlayerEntity.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server.world.entity; 2 | 3 | import java.util.random.RandomGenerator; 4 | import java.util.random.RandomGeneratorFactory; 5 | import ritzow.sandbox.util.Utility; 6 | import ritzow.sandbox.world.World; 7 | import ritzow.sandbox.world.entity.Entity; 8 | import ritzow.sandbox.world.entity.ItemEntity; 9 | import ritzow.sandbox.world.entity.PlayerEntity; 10 | 11 | public class ServerPlayerEntity extends PlayerEntity { 12 | private static final float LAUNCH_VELOCITY = Utility.convertPerSecondToPerNano(10f); 13 | 14 | private final RandomGenerator random; 15 | 16 | public ServerPlayerEntity(int entityID) { 17 | super(entityID); 18 | this.random = RandomGeneratorFactory.of("L64X128MixRandom").create(); 19 | } 20 | 21 | @Override 22 | public void onCollision(World world, Entity e, long nanoseconds) { 23 | if(e instanceof ItemEntity && e.getVelocityY() <= 0) { 24 | Utility.launchAtRandomRatio(random, e, 1/8d, 3/8d, LAUNCH_VELOCITY * random.nextFloat(1, 2)); 25 | e.setVelocityX(e.getVelocityX() + this.velocityX); 26 | } 27 | } 28 | 29 | @Override 30 | public boolean interactsWithEntities() { 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/test/ritzow/sandbox/server/test/RunTest.java: -------------------------------------------------------------------------------- 1 | package ritzow.sandbox.server.test; 2 | 3 | import java.io.IOException; 4 | import org.junit.jupiter.api.Test; 5 | import ritzow.sandbox.server.StartServer; 6 | 7 | public class RunTest { 8 | @Test 9 | void run() throws IOException { 10 | StartServer.main(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /serverlauncher/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | net.ritzow 6 | sandbox2d-parent 7 | ${revision} 8 | 9 | 10 | sandbox2d-server-launcher 11 | pom 12 | 13 | -------------------------------------------------------------------------------- /serverlauncher/prepare_build.bat: -------------------------------------------------------------------------------- 1 | ::Windows build 2 | @echo off 3 | 4 | set "output=build" 5 | 6 | @echo Delete previous build files 7 | ::clean up previous version 8 | rmdir /S /Q "%output%" 9 | mkdir "%output%" 10 | 11 | set "server=..\server" 12 | set "shared=..\shared" 13 | 14 | @echo Running jlink 15 | ::create the java runtime TODO call Build.java and move the resulting files instead of relying on Eclipse IDE build for client/shared modules 16 | "C:\Program Files\Java\jdk-12\bin\jlink.exe" ^ 17 | --compress=2 ^ 18 | --no-man-pages ^ 19 | --module-path "%server%\bin;%shared%\bin;" ^ 20 | --add-modules java.base,ritzow.sandbox.shared,ritzow.sandbox.server ^ 21 | --output "%output%\jvm" 22 | 23 | @echo Deleting unnecessary jlink output 24 | ::delete unecessary files kept by jlink 25 | set "jvmbin=%output%\jvm\bin" 26 | del "%jvmbin%\javaw.exe" 27 | del "%jvmbin%\keytool.exe" 28 | del "%output%\jvm\lib\jvm.lib" 29 | rmdir /S /Q "%output%\jvm\include" 30 | 31 | @echo Creating run script file 32 | @echo jvm\bin\java.exe ^ 33 | --module-path '../server/bin;../shared/bin;' ^ 34 | --add-modules ritzow.sandbox.shared,ritzow.sandbox.server ^ 35 | --enable-preview ^ 36 | -m ritzow.sandbox.server/ritzow.sandbox.server.StartServer > %output%\run.bat 37 | 38 | PAUSE 39 | -------------------------------------------------------------------------------- /serverlauncher/resources/greenFace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritzow/Sandbox2D/1e8d2e2dd6d76ffe5f10b304362d767e67923154/serverlauncher/resources/greenFace.png -------------------------------------------------------------------------------- /serverlauncher/resources/ui/launcher_main.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |