diff --git a/README.md b/README.md index 451da0f2c..d1b06abf3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Shattered Pixel Dungeon +This is a fork of Shattered Pixel Dungeon that can also compile for the web. Inspired by [this](https://github.com/gnojus/pixel-dungeon-gdx) web port for the original Pixel Dungeon. + +## Libraries Used +This project uses the following libraries: +- [TeaVM](https://teavm.org), licensed under the Apache License, Version 2.0. +- [gdx-teavm](https://github.com/xpenatan/gdx-teavm), licensed under the Apache License, Version 2.0. + [Shattered Pixel Dungeon](https://shatteredpixel.com/shatteredpd/) is an open-source traditional roguelike dungeon crawler with randomized levels and enemies, and hundreds of items to collect and use. It's based on the [source code of Pixel Dungeon](https://github.com/00-Evan/pixel-dungeon-gradle), by [Watabou](https://www.watabou.ru). Shattered Pixel Dungeon currently compiles for Android, iOS, and Desktop platforms. You can find official releases of the game on: diff --git a/SPD-classes/src/main/java/com/watabou/glwrap/Attribute.java b/SPD-classes/src/main/java/com/watabou/glwrap/Attribute.java index 8810dd3dc..e3e52c60f 100644 --- a/SPD-classes/src/main/java/com/watabou/glwrap/Attribute.java +++ b/SPD-classes/src/main/java/com/watabou/glwrap/Attribute.java @@ -24,6 +24,8 @@ package com.watabou.glwrap; import com.badlogic.gdx.Gdx; import java.nio.FloatBuffer; +import com.badlogic.gdx.graphics.GL20; +import com.watabou.utils.DeviceCompat; public class Attribute { @@ -36,20 +38,22 @@ public class Attribute { public int location() { return location; } + + private static final GL20 activeGL = (DeviceCompat.isWeb()) ? Gdx.gl30 : Gdx.gl; public void enable() { - Gdx.gl.glEnableVertexAttribArray( location ); + activeGL.glEnableVertexAttribArray( location ); } public void disable() { - Gdx.gl.glDisableVertexAttribArray( location ); + activeGL.glDisableVertexAttribArray( location ); } public void vertexPointer( int size, int stride, FloatBuffer ptr ) { - Gdx.gl.glVertexAttribPointer( location, size, Gdx.gl.GL_FLOAT, false, stride * 4, ptr ); + activeGL.glVertexAttribPointer( location, size, GL20.GL_FLOAT, false, stride * 4, ptr ); } public void vertexBuffer( int size, int stride, int offset) { - Gdx.gl.glVertexAttribPointer(location, size, Gdx.gl.GL_FLOAT, false, stride * 4, offset * 4); + activeGL.glVertexAttribPointer( location, size, GL20.GL_FLOAT, false, stride * 4, offset * 4 ); } } diff --git a/SPD-classes/src/main/java/com/watabou/noosa/NoosaScript.java b/SPD-classes/src/main/java/com/watabou/noosa/NoosaScript.java index 0f3416ee5..102e3320b 100644 --- a/SPD-classes/src/main/java/com/watabou/noosa/NoosaScript.java +++ b/SPD-classes/src/main/java/com/watabou/noosa/NoosaScript.java @@ -22,6 +22,8 @@ package com.watabou.noosa; import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.GL20; +import com.watabou.utils.DeviceCompat; import com.watabou.glscripts.Script; import com.watabou.glwrap.Attribute; import com.watabou.glwrap.Quad; @@ -34,6 +36,8 @@ import java.nio.ShortBuffer; public class NoosaScript extends Script { + private static final GL20 activeGL = (DeviceCompat.isWeb()) ? Gdx.gl30 : Gdx.gl20; + public Uniform uCamera; public Uniform uModel; public Uniform uTex; @@ -44,6 +48,7 @@ public class NoosaScript extends Script { private Camera lastCamera; + private int vertexBufferId; public NoosaScript() { super(); @@ -60,6 +65,12 @@ public class NoosaScript extends Script { Quad.setupIndices(); Quad.bindIndices(); + if (DeviceCompat.isWeb()) { + vertexBufferId = Gdx.gl30.glGenBuffer(); + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId); + Gdx.gl30.glBufferData(Gdx.gl30.GL_ARRAY_BUFFER, 16384, null, Gdx.gl30.GL_DYNAMIC_DRAW); + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0); + } } @Override @@ -73,27 +84,50 @@ public class NoosaScript extends Script { } public void drawElements( FloatBuffer vertices, ShortBuffer indices, int size ) { + if (DeviceCompat.isWeb()) { + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId); + ((Buffer)vertices).position( 0 ); + Gdx.gl30.glBufferSubData(Gdx.gl30.GL_ARRAY_BUFFER, 0, vertices.remaining() * 4, vertices); - ((Buffer)vertices).position( 0 ); - aXY.vertexPointer( 2, 4, vertices ); + aXY.vertexBuffer(2, 4, 0); + aUV.vertexBuffer(2, 4, 2); - ((Buffer)vertices).position( 2 ); - aUV.vertexPointer( 2, 4, vertices ); + Quad.releaseIndices(); + Gdx.gl30.glDrawElements( Gdx.gl30.GL_TRIANGLES, size, Gdx.gl30.GL_UNSIGNED_SHORT, indices ); + Quad.bindIndices(); + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0); + } else { + ((Buffer)vertices).position( 0 ); + aXY.vertexPointer( 2, 4, vertices ); - Quad.releaseIndices(); - Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, size, Gdx.gl20.GL_UNSIGNED_SHORT, indices ); - Quad.bindIndices(); + ((Buffer)vertices).position( 2 ); + aUV.vertexPointer( 2, 4, vertices ); + + Quad.releaseIndices(); + Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, size, Gdx.gl20.GL_UNSIGNED_SHORT, indices ); + Quad.bindIndices(); + } } public void drawQuad( FloatBuffer vertices ) { + if (DeviceCompat.isWeb()) { + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId); + ((Buffer)vertices).position( 0 ); + Gdx.gl30.glBufferSubData(Gdx.gl30.GL_ARRAY_BUFFER, 0, vertices.remaining() * 4, vertices); - ((Buffer)vertices).position( 0 ); - aXY.vertexPointer( 2, 4, vertices ); + Gdx.gl30.glVertexAttribPointer(aXY.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 0); + Gdx.gl30.glVertexAttribPointer(aUV.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 2 * 4); + Gdx.gl30.glDrawElements( Gdx.gl30.GL_TRIANGLES, Quad.SIZE, Gdx.gl30.GL_UNSIGNED_SHORT, 0 ); + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0); + } else { + ((Buffer)vertices).position( 0 ); + aXY.vertexPointer( 2, 4, vertices ); - ((Buffer)vertices).position( 2 ); - aUV.vertexPointer( 2, 4, vertices ); + ((Buffer)vertices).position( 2 ); + aUV.vertexPointer( 2, 4, vertices ); - Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); + Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); + } } public void drawQuad( Vertexbuffer buffer ) { @@ -107,7 +141,7 @@ public class NoosaScript extends Script { buffer.release(); - Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); + activeGL.glDrawElements( GL20.GL_TRIANGLES, Quad.SIZE, GL20.GL_UNSIGNED_SHORT, 0 ); } public void drawQuadSet( FloatBuffer vertices, int size ) { @@ -116,13 +150,26 @@ public class NoosaScript extends Script { return; } - ((Buffer)vertices).position( 0 ); - aXY.vertexPointer( 2, 4, vertices ); + if (DeviceCompat.isWeb()) { + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, vertexBufferId); + ((Buffer)vertices).position( 0 ); + Gdx.gl30.glBufferSubData(Gdx.gl30.GL_ARRAY_BUFFER, 0, vertices.remaining() * 4, vertices); - ((Buffer)vertices).position( 2 ); - aUV.vertexPointer( 2, 4, vertices ); + Gdx.gl30.glVertexAttribPointer(aXY.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 0); + Gdx.gl30.glVertexAttribPointer(aUV.location(), 2, Gdx.gl30.GL_FLOAT, false, 4 * 4, 2 * 4); + + Gdx.gl30.glDrawElements( Gdx.gl30.GL_TRIANGLES, Quad.SIZE * size, Gdx.gl30.GL_UNSIGNED_SHORT, 0 ); + + Gdx.gl30.glBindBuffer(Gdx.gl30.GL_ARRAY_BUFFER, 0); + } else { + ((Buffer)vertices).position( 0 ); + aXY.vertexPointer( 2, 4, vertices ); + + ((Buffer)vertices).position( 2 ); + aUV.vertexPointer( 2, 4, vertices ); - Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE * size, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); + Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE * size, Gdx.gl20.GL_UNSIGNED_SHORT, 0 ); + } } public void drawQuadSet( Vertexbuffer buffer, int length, int offset ){ @@ -140,7 +187,7 @@ public class NoosaScript extends Script { buffer.release(); - Gdx.gl20.glDrawElements( Gdx.gl20.GL_TRIANGLES, Quad.SIZE * length, Gdx.gl20.GL_UNSIGNED_SHORT, Quad.SIZE * Short.SIZE/8 * offset ); + activeGL.glDrawElements( GL20.GL_TRIANGLES, Quad.SIZE * length, GL20.GL_UNSIGNED_SHORT, Quad.SIZE * Short.SIZE/8 * offset ); } public void lighting( float rm, float gm, float bm, float am, float ra, float ga, float ba, float aa ) { @@ -161,7 +208,7 @@ public class NoosaScript extends Script { uCamera.valueM4( camera.matrix ); if (!camera.fullScreen) { - Gdx.gl20.glEnable( Gdx.gl20.GL_SCISSOR_TEST ); + activeGL.glEnable( GL20.GL_SCISSOR_TEST ); //This fixes pixel scaling issues on some hidpi displays (mainly on macOS) // because for some reason all other openGL operations work on virtual pixels @@ -169,13 +216,13 @@ public class NoosaScript extends Script { float xScale = (Gdx.graphics.getBackBufferWidth() / (float)Game.width ); float yScale = ((Gdx.graphics.getBackBufferHeight()-Game.bottomInset) / (float)Game.height ); - Gdx.gl20.glScissor( + activeGL.glScissor( Math.round(camera.x * xScale), Math.round((Game.height - camera.screenHeight - camera.y) * yScale) + Game.bottomInset, Math.round(camera.screenWidth * xScale), Math.round(camera.screenHeight * yScale)); } else { - Gdx.gl20.glDisable( Gdx.gl20.GL_SCISSOR_TEST ); + activeGL.glDisable( GL20.GL_SCISSOR_TEST ); } } } @@ -189,8 +236,41 @@ public class NoosaScript extends Script { return SHADER; } - private static final String SHADER = + private static String SHADER; + static { + if (DeviceCompat.isWeb()) { + SHADER = + //vertex shader + "#version 300 es\n" + + "uniform mat4 uCamera;\n" + + "uniform mat4 uModel;\n" + + "in vec4 aXYZW;\n" + + "in vec2 aUV;\n" + + "out vec2 vUV;\n" + + "void main() {\n" + + " gl_Position = uCamera * uModel * aXYZW;\n" + + " vUV = aUV;\n" + + "}\n" + + + //this symbol separates the vertex and fragment shaders (see Script.compile) + "//\n" + + //fragment shader + //preprocessor directives let us define precision on GLES platforms, and ignore it elsewhere + "#version 300 es\n" + + "#ifdef GL_ES\n" + + " precision mediump float;\n" + + "#endif\n" + + "in vec2 vUV;\n" + + "uniform sampler2D uTex;\n" + + "uniform vec4 uColorM;\n" + + "uniform vec4 uColorA;\n" + + "out vec4 fragColor;\n" + + "void main() {\n" + + " fragColor = texture( uTex, vUV ) * uColorM + uColorA;\n" + + "}\n"; + } else { + SHADER = //vertex shader "uniform mat4 uCamera;\n" + "uniform mat4 uModel;\n" + @@ -217,4 +297,6 @@ public class NoosaScript extends Script { "void main() {\n" + " gl_FragColor = texture2D( uTex, vUV ) * uColorM + uColorA;\n" + "}\n"; + } + } } diff --git a/SPD-classes/src/main/java/com/watabou/noosa/NoosaScriptNoLighting.java b/SPD-classes/src/main/java/com/watabou/noosa/NoosaScriptNoLighting.java index 689a8d2f5..0c72a51aa 100644 --- a/SPD-classes/src/main/java/com/watabou/noosa/NoosaScriptNoLighting.java +++ b/SPD-classes/src/main/java/com/watabou/noosa/NoosaScriptNoLighting.java @@ -22,6 +22,7 @@ package com.watabou.noosa; import com.watabou.glscripts.Script; +import com.watabou.utils.DeviceCompat; //This class should be used on heavy pixel-fill based loads when lighting is not needed. // It skips the lighting component of the fragment shader, giving a significant performance boost @@ -44,8 +45,40 @@ public class NoosaScriptNoLighting extends NoosaScript { return SHADER; } - private static final String SHADER = - + private static String SHADER; + + static { + if (DeviceCompat.isWeb()) { + SHADER = + //vertex shader + "#version 300 es\n" + + "uniform mat4 uCamera;\n" + + "uniform mat4 uModel;\n" + + "in vec4 aXYZW;\n" + + "in vec2 aUV;\n" + + "out vec2 vUV;\n" + + "void main() {\n" + + " gl_Position = uCamera * uModel * aXYZW;\n" + + " vUV = aUV;\n" + + "}\n" + + + //this symbol separates the vertex and fragment shaders (see Script.compile) + "//\n" + + + //fragment shader + //preprocessor directives let us define precision on GLES platforms, and ignore it elsewhere + "#version 300 es\n" + + "#ifdef GL_ES\n" + + " precision mediump float;\n" + + "#endif\n" + + "in vec2 vUV;\n" + + "uniform sampler2D uTex;\n" + + "out vec4 fragColor;\n" + + "void main() {\n" + + " fragColor = texture( uTex, vUV );\n" + + "}\n"; + } else { + SHADER = //vertex shader "uniform mat4 uCamera;\n" + "uniform mat4 uModel;\n" + @@ -70,4 +103,6 @@ public class NoosaScriptNoLighting extends NoosaScript { "void main() {\n" + " gl_FragColor = texture2D( uTex, vUV );\n" + "}\n"; + } + } } diff --git a/SPD-classes/src/main/java/com/watabou/noosa/RenderedText.java b/SPD-classes/src/main/java/com/watabou/noosa/RenderedText.java index 0cc0648e4..18b69e6bd 100644 --- a/SPD-classes/src/main/java/com/watabou/noosa/RenderedText.java +++ b/SPD-classes/src/main/java/com/watabou/noosa/RenderedText.java @@ -32,6 +32,7 @@ import com.badlogic.gdx.math.Affine2; import com.badlogic.gdx.math.Matrix4; import com.watabou.glwrap.Matrix; import com.watabou.glwrap.Quad; +import com.watabou.utils.DeviceCompat; import java.nio.Buffer; import java.nio.FloatBuffer; @@ -79,9 +80,11 @@ public class RenderedText extends Image { private synchronized void measure(){ + if (!DeviceCompat.isWeb()) { if (Thread.currentThread().getName().equals("SHPD Actor Thread")){ throw new RuntimeException("Text measured from the actor thread!"); } + } if ( text == null || text.equals("") ) { text = ""; diff --git a/SPD-classes/src/main/java/com/watabou/noosa/Tilemap.java b/SPD-classes/src/main/java/com/watabou/noosa/Tilemap.java index 46782fb91..1e1447f23 100644 --- a/SPD-classes/src/main/java/com/watabou/noosa/Tilemap.java +++ b/SPD-classes/src/main/java/com/watabou/noosa/Tilemap.java @@ -27,6 +27,7 @@ import com.watabou.glwrap.Quad; import com.watabou.glwrap.Vertexbuffer; import com.watabou.utils.Rect; import com.watabou.utils.RectF; +import com.watabou.utils.DeviceCompat;; import java.nio.Buffer; import java.nio.FloatBuffer; @@ -197,7 +198,9 @@ public class Tilemap extends Visual { public void draw() { super.draw(); - + if (DeviceCompat.isWeb()) { + fullUpdate = true; + } if (!updated.isEmpty()) { updateVertices(); if (buffer == null) diff --git a/SPD-classes/src/main/java/com/watabou/utils/Bundle.java b/SPD-classes/src/main/java/com/watabou/utils/Bundle.java index 70d2a01b2..d65e16ce1 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/Bundle.java +++ b/SPD-classes/src/main/java/com/watabou/utils/Bundle.java @@ -25,6 +25,7 @@ import com.badlogic.gdx.utils.JsonReader; import com.badlogic.gdx.utils.JsonValue; import com.badlogic.gdx.utils.JsonWriter; import com.watabou.noosa.Game; +import com.watabou.utils.DeviceCompat; import org.json.JSONArray; import org.json.JSONException; @@ -484,7 +485,15 @@ public class Bundle { } //useful to turn this off for save data debugging. - private static final boolean compressByDefault = true; + private static final boolean compressByDefault; + + static { + if (DeviceCompat.isWeb()) { + compressByDefault = false; + } else { + compressByDefault = true; + } + } private static final int GZIP_BUFFER = 1024*4; //4 kb diff --git a/SPD-classes/src/main/java/com/watabou/utils/DeviceCompat.java b/SPD-classes/src/main/java/com/watabou/utils/DeviceCompat.java index e7d86d476..f457da3ea 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/DeviceCompat.java +++ b/SPD-classes/src/main/java/com/watabou/utils/DeviceCompat.java @@ -25,6 +25,7 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.utils.SharedLibraryLoader; import com.watabou.noosa.Game; +import com.badlogic.gdx.Application; //TODO migrate to platformSupport class public class DeviceCompat { @@ -49,15 +50,22 @@ public class DeviceCompat { } public static boolean isAndroid(){ - return SharedLibraryLoader.isAndroid; + return Gdx.app.getType() == Application.ApplicationType.Android; } public static boolean isiOS(){ - return SharedLibraryLoader.isIos; + return Gdx.app.getType() == Application.ApplicationType.iOS; } public static boolean isDesktop(){ - return SharedLibraryLoader.isWindows || SharedLibraryLoader.isMac || SharedLibraryLoader.isLinux; + return System.getProperty("os.name").toLowerCase().contains("win") || + System.getProperty("os.name").toLowerCase().contains("mac") || + System.getProperty("os.name").toLowerCase().contains("nux") || + Gdx.app.getType() == Application.ApplicationType.WebGL; + } + + public static boolean isWeb(){ + return Gdx.app.getType() == Application.ApplicationType.WebGL; } public static boolean hasHardKeyboard(){ diff --git a/build.gradle b/build.gradle index 20c6c362a..6b1ed8d1a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,9 @@ allprojects { gdxVersion = '1.12.1' gdxControllersVersion = '2.2.4-SNAPSHOT' robovmVersion = '2.3.21' + + gdxTeaVMVersion = '1.0.5' + teaVMVersion = '0.11.0' } version = appVersionName diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Rankings.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Rankings.java index 816639bd0..4607a8401 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Rankings.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Rankings.java @@ -43,6 +43,7 @@ import com.watabou.noosa.Game; import com.watabou.utils.Bundlable; import com.watabou.utils.Bundle; import com.watabou.utils.FileUtils; +import com.watabou.utils.DeviceCompat;; import java.io.IOException; import java.text.DateFormat; @@ -56,6 +57,7 @@ import java.util.Locale; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.Random; public enum Rankings { @@ -64,6 +66,11 @@ public enum Rankings { public static final int TABLE_SIZE = 11; public static final String RANKINGS_FILE = "rankings.dat"; + + private static String generateUUID() { + Random random = new Random(); + return Long.toHexString(random.nextLong()) + "-" + Long.toHexString(random.nextLong()); + } public ArrayList records; public int lastRecord; @@ -116,7 +123,11 @@ public enum Rankings { INSTANCE.saveGameData(rec); + if (DeviceCompat.isWeb()) { + rec.gameID = generateUUID(); + } else { rec.gameID = UUID.randomUUID().toString(); + } if (rec.daily){ if (Dungeon.dailyReplay){ @@ -533,7 +544,11 @@ public enum Rankings { if (bundle.contains(DATA)) gameData = bundle.getBundle(DATA); if (bundle.contains(ID)) gameID = bundle.getString(ID); + if (DeviceCompat.isWeb()) { + if (gameID == null) gameID = generateUUID(); + } else { if (gameID == null) gameID = UUID.randomUUID().toString(); + } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java index 497719af6..6ee0f9ffe 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/scenes/GameScene.java @@ -643,11 +643,13 @@ public class GameScene extends PixelScene { public void destroy() { //tell the actor thread to finish, then wait for it to complete any actions it may be doing. + if (!DeviceCompat.isWeb()) { if (!waitForActorThread( 4500, true )){ Throwable t = new Throwable(); t.setStackTrace(actorThread.getStackTrace()); throw new RuntimeException("timeout waiting for actor thread! ", t); } + } Emitter.freezeEmitters = false; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/changelist/v3_X_Changes.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/changelist/v3_X_Changes.java index a04fba3df..9ae36ee78 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/changelist/v3_X_Changes.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/changelist/v3_X_Changes.java @@ -89,7 +89,7 @@ public class v3_X_Changes { changes.hardlight(Window.TITLE_COLOR); changeInfos.add(changes); - if (DeviceCompat.isDesktop() && SharedLibraryLoader.isLinux) { + if (DeviceCompat.isDesktop() && System.getProperty("os.name").toLowerCase().contains("nux")) { changes.addButton(new ChangeButton(Icons.DISPLAY.get(), "A Note for Steam Deck users", "A bug was fixed in this patch which affected display scaling on Steam Deck. Due to a quirk in how the Steam Deck reported display dimensions, the game incorrectly thought Steam Deck's screen was about 4\", instead of 7\".\n" + "\n" + diff --git a/html/build.gradle b/html/build.gradle new file mode 100644 index 000000000..1b5c066be --- /dev/null +++ b/html/build.gradle @@ -0,0 +1,154 @@ +apply plugin: 'java-library' + +dependencies { + implementation "com.badlogicgames.gdx:gdx:$gdxVersion" + implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion" + implementation "com.github.xpenatan.gdx-teavm:backend-teavm:$gdxTeaVMVersion" + implementation "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" + implementation "com.github.xpenatan.gdx-teavm:gdx-freetype-teavm:$gdxTeaVMVersion" + implementation project(':core') + implementation "org.teavm:teavm-jso:$teaVMVersion" + implementation "org.teavm:teavm-core:$teaVMVersion" +} + +repositories { + google() + mavenCentral() + maven { url 'https://teavm.org/maven/repository' } +} + +tasks.register('compileClient', JavaExec) { + classpath(sourceSets.main.runtimeClasspath) + mainClass.set('com.shatteredpixel.shatteredpixeldungeon.html.Compile') + jvmArgs('-Xms8g', '-Xmx14g', '-Xss16m', '-XX:MaxMetaspaceSize=12g', '-Dfile.encoding=UTF-8') + outputs.dir '../release/webapp' + doFirst { + println "Running compileClient..." + } + doLast { + println "compileClient completed!" + } +} + +task modifyHtml { + doLast { + def htmlFile = file("../release/webapp/index.html") + def destinationDir = file("../release/webapp/assets") + def sourceFavicon = file("../desktop/src/main/assets/icons/windows.ico") + def sourceGif = file("../html/src/main/assets/logo.gif") + def destinationStartupLogo = file("../release/webapp/startup-logo.png") + + // copy windows.ico + if (sourceFavicon.exists()) { + def destinationFile = new File(destinationDir, "windows.ico") + sourceFavicon.withInputStream { input -> + destinationFile.withOutputStream { output -> + output << input + } + } + println "Copied windows.ico to assets folder." + } else { + println "windows.ico does not exist. Skipping copy step." + } + + if (sourceGif.exists()) { + destinationStartupLogo.parentFile.mkdirs() + sourceGif.withInputStream { input -> + destinationStartupLogo.withOutputStream { output -> + output << input + } + } + println "Replaced startup-logo.png with a gif." + } else { + println "Gif does not exist. Skipping replacement step." + } + + if (htmlFile.exists()) { + def faviconLink = '' + def content = htmlFile.text + + // add a link for the favicon + if (content.contains("")) { + content = content.replace("", faviconLink + "\n") + println "Favicon link added to index.html." + } + + // Change page title + if (content.contains("")) { + content = content.replaceFirst("<title>.*?", "Shattered Pixel Dungeon") + println "Title updated to 'Shattered Pixel Dungeon'." + } else { + println "No tag found in index.html. Skipping title update." + } + + if (content.contains("<style>") && content.contains("</style>")) { + content = content.replaceAll( + "(?s)<style>.*?</style>", + """<style> + body { + display: flex; + justify-content: center; + align-items: center; + background: #000; + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; + } + #progress { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + } + #progress-img { + display: flex; + width: 850px; + height: auto; + margin-bottom: -20px; + } + #progress-box { + background: rgba(255,255,255,0.1); + border: 2px solid #fff; + display: flex; + margin-top: -20px; + justify-content: center; + align-items: center; + position: relative; + height: 30px; + width: 600px; + } + #progress-bar { + display: block; + background: #44FF82; + height: 20px; + width: 0%; + } + </style>""" + ) + + println "Successfully replaced the <style> block in index.html." + } else { + println "No <style> block found in index.html. Skipping style replacement." + } + + htmlFile.text = content + } else { + println "index.html does not exist. Skipping favicon injection." + } + } +} + +tasks.named('modifyHtml') { + mustRunAfter('compileClient') +} + +tasks.named('build') { + dependsOn 'compileClient' + finalizedBy 'modifyHtml' +} diff --git a/html/buildTestWeb.sh b/html/buildTestWeb.sh new file mode 100755 index 000000000..b81ed30d9 --- /dev/null +++ b/html/buildTestWeb.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +../gradlew :html:build +wait +if [[ -d ../release/webapp/ ]]; then + python3 -m http.server 8100 -d ../release/webapp/ +fi diff --git a/html/src/main/assets/logo.gif b/html/src/main/assets/logo.gif new file mode 100644 index 000000000..be7ff3002 Binary files /dev/null and b/html/src/main/assets/logo.gif differ diff --git a/html/src/main/java/Compile.java b/html/src/main/java/Compile.java new file mode 100644 index 000000000..8405f945c --- /dev/null +++ b/html/src/main/java/Compile.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * This file is derived from micronaut-libgdx-teavm (https://github.com/hollingsworthd/micronaut-libgdx-teavm), + * originally licensed under the Apache License, Version 2.0. + * + * Modifications made by Konsthol on 13/4/25: + * - Adjusted to compile Shattered Pixel Dungeon + * + * Copyright 2022 Daniel Hollingsworth + */ + +package com.shatteredpixel.shatteredpixeldungeon.html; + +import com.github.xpenatan.gdx.backends.teavm.config.AssetFileHandle; +import com.github.xpenatan.gdx.backends.teavm.config.TeaBuildConfiguration; +import com.github.xpenatan.gdx.backends.teavm.config.TeaBuilder; +import com.github.xpenatan.gdx.backends.teavm.gen.SkipClass; +import java.io.File; +import java.io.IOException; +import org.teavm.tooling.TeaVMTool; +import org.teavm.vm.TeaVMOptimizationLevel; + +@SkipClass +public class Compile { + + public static void main(String[] args) throws IOException { + deleteDir(new File("../release/webapp")); + + TeaBuildConfiguration conf = new TeaBuildConfiguration(); + + conf.webappPath = new File("../release").getAbsolutePath(); + conf.assetsPath.add(new AssetFileHandle("../core/src/main/assets")); + + TeaVMTool tool = TeaBuilder.config(conf); + tool.setMainClass(TeaVMLauncher.class.getName()); + tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); + tool.setObfuscated(true); + tool.setShortFileNames(true); + tool.setSourceFilesCopied(false); + tool.setStrict(false); + tool.setSourceMapsFileGenerated(false); + tool.setDebugInformationGenerated(false); + tool.setIncremental(false); + + TeaBuilder.build(tool); + } + + private static void deleteDir(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + deleteDir(file); + } + } + dir.delete(); + } +} diff --git a/html/src/main/java/HtmlPlatformSupport.java b/html/src/main/java/HtmlPlatformSupport.java new file mode 100644 index 000000000..4c6d60180 --- /dev/null +++ b/html/src/main/java/HtmlPlatformSupport.java @@ -0,0 +1,122 @@ +package com.shatteredpixel.shatteredpixeldungeon.html; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.g2d.PixmapPacker; +import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator; +import com.shatteredpixel.shatteredpixeldungeon.SPDSettings; +import com.watabou.input.ControllerHandler; +import com.watabou.noosa.Game; +import com.watabou.utils.PlatformSupport; +import org.teavm.jso.JSBody; + +import java.util.HashMap; +import java.util.regex.Pattern; + +public class HtmlPlatformSupport extends PlatformSupport { + + @Override + public void updateDisplaySize() { + if (Gdx.app == null || Gdx.graphics == null) { + System.err.println("Gdx is not initialized yet. Skipping display size update."); + return; + } + + Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + } + + @Override + public void updateSystemUI() { + Gdx.app.postRunnable( new Runnable() { + @Override + public void run() { + Object canvas = getCanvasElement("canvas"); + if (SPDSettings.fullscreen()) { + System.out.println("Requesting fullscreen."); + requestFullscreen("canvas"); + } else { + System.out.println("Exiting fullscreen."); + exitFullscreen(); + setCanvasSize("canvas", SPDSettings.windowResolution().x, SPDSettings.windowResolution().y); + } + } + }); + } + + @JSBody(script = "" + + "document.body.addEventListener('click', function() {" + + " console.log('User clicked. Triggering fullscreen and audio...');" + + " document.getElementById('canvas').requestFullscreen();" + + " if (!window.audioContext) {" + + " window.audioContext = new (window.AudioContext || window.webkitAudioContext)();" + + " }" + + " window.audioContext.resume().then(() => {" + + " console.log('AudioContext resumed successfully.');" + + " }).catch((err) => {" + + " console.error('Failed to resume AudioContext:', err);" + + " });" + + "}, { once: true });") + public static native void setupClickListener(); + + @JSBody(params = {"id"}, script = "document.getElementById(id).requestFullscreen();") + private static native void requestFullscreen(String id); + + @JSBody(params = {"id"}, script = "return document.getElementById(id);") + private static native Object getCanvasElement(String id); + + @JSBody(params = {}, script = "document.exitFullscreen();") + private static native void exitFullscreen(); + + @JSBody(params = {"id", "width", "height"}, script = "var canvas = document.getElementById(id); canvas.width = width; canvas.height = height;") + private static native void setCanvasSize(String id, int width, int height); + + @Override + public boolean connectedToUnmeteredNetwork() { + return true; + } + + @Override + public boolean supportsVibration() { + return ControllerHandler.vibrationSupported(); + } + + private static FreeTypeFontGenerator basicFontGenerator; + + @Override + public void setupFontGenerators(int pageSize, boolean systemfont) { + //don't bother doing anything if nothing has changed + if (fonts != null && this.pageSize == pageSize && this.systemfont == systemfont) { + return; + } + this.pageSize = pageSize; + this.systemfont = systemfont; + + resetGenerators(false); + fonts = new HashMap<>(); + + basicFontGenerator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/pixel_font.ttf")); + + fonts.put(basicFontGenerator, new HashMap<>()); + + packer = new PixmapPacker(pageSize, pageSize, Pixmap.Format.RGBA8888, 1, false); + } + + @Override + protected FreeTypeFontGenerator getGeneratorForString( String input ) { + return basicFontGenerator; + } + + private final Pattern regularsplitter = Pattern.compile("(?<=\n)|(?=\n)|(?<=_)|(?=_)|(?<=\\*\\*)|(?=\\*\\*)"); + + private final Pattern regularsplitterMultiline = Pattern.compile("(?<= )|(?= )|(?<=\n)|(?=\n)|(?<=_)|(?=_)|(?<=\\*\\*)|(?=\\*\\*)"); + + @Override + public String[] splitforTextBlock(String text, boolean multiline) { + if (multiline) { + return regularsplitterMultiline.split(text); + } else { + return regularsplitter.split(text); + } + } +} diff --git a/html/src/main/java/TeaVMLauncher.java b/html/src/main/java/TeaVMLauncher.java new file mode 100644 index 000000000..a40e31929 --- /dev/null +++ b/html/src/main/java/TeaVMLauncher.java @@ -0,0 +1,46 @@ +package com.shatteredpixel.shatteredpixeldungeon.html; + +import com.badlogic.gdx.Files; +import com.watabou.noosa.Game; +import com.shatteredpixel.shatteredpixeldungeon.ShatteredPixelDungeon; +import com.github.xpenatan.gdx.backends.teavm.TeaApplicationConfiguration; +import com.github.xpenatan.gdx.backends.teavm.TeaApplication; +import com.watabou.utils.FileUtils; + +public class TeaVMLauncher { + + public static void main(String[] args) { + TeaApplicationConfiguration config = new TeaApplicationConfiguration("canvas"); + config.width = 0; + config.height = 0; + config.useGL30 = true; + config.preloadListener = assetLoader -> { + try { + assetLoader.loadScript("freetype.js"); + } catch (Exception e) { + System.err.println("Error loading freetype.js: " + e.getMessage()); + } + }; + + initializeServices(); + FileUtils.setDefaultFileProperties(Files.FileType.Local, ""); + + try { + + HtmlPlatformSupport platformSupport = new HtmlPlatformSupport(); + + platformSupport.setupClickListener(); + + new TeaApplication(new ShatteredPixelDungeon(platformSupport), config); + + } catch (Exception e) { + System.err.println("Error launching TeaApplication: " + e.getMessage()); + e.printStackTrace(); + } + } + + private static void initializeServices() { + Game.version = "3.0.2"; + Game.versionCode = 833; + } +} diff --git a/settings.gradle b/settings.gradle index 579157ae5..c6580cb4d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,7 @@ include ':core' include ':android' include ':ios' include ':desktop' +include ':html' //service modules include ':services'