diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index aec440c67..cc424ff18 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,6 +12,7 @@ jobs: permissions: actions: read contents: read + pull-requests: write security-events: write steps: @@ -33,8 +34,30 @@ jobs: run: ./gradlew build - name: Lint + id: lint + continue-on-error: true + run: ./gradlew lint + + - name: Count lint violations + id: count_violations + if: steps.lint.outcome == 'failure' run: | - ./gradlew lint || echo "::warning::Checkstyle found violations. See the checkstyle-report artifact for details." + count=$(grep -r '> $GITHUB_OUTPUT + + - name: Comment on PR if lint failed + if: steps.lint.outcome == 'failure' && github.event_name == 'pull_request' + uses: actions/github-script@v6 + with: + script: | + const count = ${{ steps.count_violations.outputs.count }}; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `:warning: **Checkstyle found ${count} style violation${count === 1 ? '' : 's'}.** Download the **checkstyle-report** artifact from the [Actions run](${runUrl}) for details.` + }) - name: Determine artifact name id: find_artifact diff --git a/build.gradle.kts b/build.gradle.kts index 634eeda1c..294c9af61 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,14 +13,15 @@ fun getHash(): String { ?.trim() ?: "00000000" } -val lwjglVersion = "3.4.1" +// Weird Parth's desktop stuff, remove before merge +val lwjglVersion = "3.3.3" val lwjglNatives = listOf( - "natives-freebsd", - "natives-linux-arm32", "natives-linux-arm64", - "natives-linux-ppc64le", "natives-linux-riscv64", +// "natives-freebsd", +// "natives-linux-arm32", "natives-linux-arm64", +// "natives-linux-ppc64le", "natives-linux-riscv64", "natives-linux", - "natives-macos", "natives-macos-arm64", - "natives-windows-x86", "natives-windows", "natives-windows-arm64", +// "natives-macos", "natives-macos-arm64", +// "natives-windows-x86", "natives-windows", "natives-windows-arm64", ) repositories { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0aaefbcaf..1f777c357 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ +#Fri Apr 17 23:09:51 EDT 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/lwjglwindow/LWJGLWindow.java b/src/main/java/lwjglwindow/LWJGLWindow.java index 1664f000f..582f5a41b 100644 --- a/src/main/java/lwjglwindow/LWJGLWindow.java +++ b/src/main/java/lwjglwindow/LWJGLWindow.java @@ -168,6 +168,9 @@ protected void init() GLFWErrorCallback.createPrint(System.err).set(); + if (System.getProperty("os.name").toLowerCase().contains("linux")) + GLFW.glfwInitHint(GLFW.GLFW_PLATFORM, GLFW.GLFW_PLATFORM_X11); + if (!glfwInit()) throw new IllegalStateException("Unable to initialize GLFW"); diff --git a/src/main/java/tanks/Crusade.java b/src/main/java/tanks/Crusade.java index dbad61631..4c9994b6f 100644 --- a/src/main/java/tanks/Crusade.java +++ b/src/main/java/tanks/Crusade.java @@ -8,6 +8,7 @@ import tanks.network.ServerHandler; import tanks.network.event.*; import tanks.tank.*; +import tanks.tankson.*; import java.util.*; @@ -294,7 +295,17 @@ public void begin() public void loadLevel() { - Level l = new Level(this.levels.get(this.currentLevel).levelString, this.customTanks); + Level l; + try + { + l = (Level) Serializer.fromTanksON(this.levels.get(this.currentLevel).levelString); + l.init(customTanks); + l.levelString = this.levels.get(this.currentLevel).levelString; + } + catch (RuntimeException e) + { + l = new Level(this.levels.get(this.currentLevel).levelString, customTanks); + } Game.player.hotbar.enabledCoins = true; Game.player.hotbar.itemBar.showItems = true; diff --git a/src/main/java/tanks/Game.java b/src/main/java/tanks/Game.java index 1f6924920..2118e24d0 100644 --- a/src/main/java/tanks/Game.java +++ b/src/main/java/tanks/Game.java @@ -25,6 +25,7 @@ import tanks.registry.*; import tanks.rendering.*; import tanks.tank.*; +import tanks.tankson.Serializer; import com.codedisaster.steamworks.SteamMatchmaking; @@ -1373,7 +1374,17 @@ public static boolean loadLevel(BaseFile f, ILevelPreviewScreen s) line.append(f.nextLine()).append("\n"); } - Level l = new Level(line.substring(0, line.length() - 1)); + Level l; + try + { + l = (Level) Serializer.fromTanksON(line.substring(0, line.length() - 1)); + l.init(); + l.levelString = line.substring(0, line.length() - 1); + } + catch (RuntimeException e) + { + l = new Level(line.substring(0, line.length() - 1)); + } l.loadLevel(s); f.stopReading(); @@ -1388,8 +1399,18 @@ public static boolean loadLevel(BaseFile f, ILevelPreviewScreen s) public static int compareVersions(String v1, String v2) { - String[] a = v1.substring(v1.indexOf(" v") + 2).split("\\."); - String[] b = v2.substring(v2.indexOf(" v") + 2).split("\\."); + String[] a; + String[] b; + if (v1.contains(" v") && v2.contains(" v")) + { + a = v1.substring(v1.indexOf(" v") + 2).split("\\."); + b = v2.substring(v2.indexOf(" v") + 2).split("\\."); + } + else + { + a = v1.split("\\."); + b = v2.split("\\."); + } for (int i = 0; i < Math.max(a.length, b.length); i++) { diff --git a/src/main/java/tanks/Level.java b/src/main/java/tanks/Level.java index 2754004b1..43c5fb74d 100644 --- a/src/main/java/tanks/Level.java +++ b/src/main/java/tanks/Level.java @@ -8,21 +8,26 @@ import tanks.item.Item; import tanks.network.ServerHandler; import tanks.network.event.*; -import tanks.obstacle.Obstacle; -import tanks.obstacle.ObstacleBeatBlock; +import tanks.obstacle.*; import tanks.registry.RegistryTank; import tanks.tank.*; +import tanks.tankson.*; import java.util.*; +@TanksONable("level") public class Level { public String levelString; + @Property(id = "tank_pos", name = "Tank Positions") + public ArrayList> tanksIR; public ArrayList tanks; ArrayList tanksToRemove; public Team[] tankTeams; public ArrayList obstacles; + @Property(id = "obstacles", name = "Obstacles") + public ArrayList> obstaclesIR; public boolean enableTeams = false; public static Color currentColor = new Color(235, 207, 166); @@ -34,25 +39,34 @@ public class Level public static Random random = new Random(); + @Property(id = "editable", name = "Editable") public boolean editable = true; public boolean remote = false; public boolean preview = false; + @Property(id = "timer", name = "Timer") public double timer = -1; public int startX; public int startY; + @Property(id = "size_x", name = "Size X") public int sizeX; + @Property(id = "size_y", name = "Size Y") public int sizeY; + @Property(id = "color", name = "Color") public Color color = new Color(235, 207, 166); + @Property(id = "color_var", name = "Color Variation") public Color colorVar = new Color(20, 20, 20); public int tilesRandomSeed = (int) (Math.random() * Integer.MAX_VALUE); + @Property(id = "light", name = "Light") public double light = 1.0; + @Property(id = "shadow", name = "Shadow") public double shadow = 0.5; + @Property(id = "teams", name = "Teams") public LinkedHashMap teamsMap = new LinkedHashMap<>(); public ArrayList availablePlayerSpawns = new ArrayList<>(); @@ -64,9 +78,13 @@ public class Level public ArrayList includedPlayers = new ArrayList<>(); + @Property(id = "coins", name = "Coins") public int startingCoins; + @Property(id = "Shop", name = "Shop") public ArrayList shop = new ArrayList<>(); + @Property(id = "items", name = "Starting Items") public ArrayList> startingItems = new ArrayList<>(); + @Property(id = "builds", name = "Player Builds") public ArrayList playerBuilds = new ArrayList<>(); // Saved on the client to keep track of what each item is @@ -74,6 +92,7 @@ public class Level public ArrayList clientShop = new ArrayList<>(); public ArrayList> clientStartingItems = new ArrayList<>(); + @Property(id = "custom_tanks", name = "Custom Tanks") public ArrayList customTanks; public LinkedHashMap itemNumbers = new LinkedHashMap<>(); @@ -86,6 +105,16 @@ public class Level public HashMap tankLookupTable = null; + public Level() + { + obstacles = new ArrayList(); + this.tanks = new ArrayList(); + this.customTanks = new ArrayList<>(); + + this.remote = false; + this.disableFriendlyFire = false; + } + public Level(String level) { this(level, false); @@ -110,6 +139,11 @@ public Level(String level, ArrayList customTanks) */ public Level(String level, ArrayList customTanks, boolean remote, boolean disableFriendlyFire) { + obstacles = new ArrayList<>(); + this.tanks = new ArrayList<>(); + this.obstaclesIR = new ArrayList<>(); + this.tanksIR = new ArrayList<>(); + this.disableFriendlyFire = disableFriendlyFire; this.remote = remote; @@ -127,8 +161,6 @@ public Level(String level, ArrayList customTanks, boolean remo //Look Ahead Split (keeping the delimiter with the associated block) String[] blocks = this.levelString.split("(?=(level|items|shop|coins|tanks|builds)\n)"); - obstacles = new ArrayList(); - this.tanks = new ArrayList(); for (String s: blocks) { @@ -277,6 +309,17 @@ else if (t.length >= 2) { String[] obs = obstaclesPo.split("-"); + ArrayList obsIR = new ArrayList<>(); + obsIR.add(obs[0].replace("...", ":")); //X Coordinate (changed to sliced notation) + obsIR.add(obs[1].replace("...", ":")); //Y Coordinate (changed to sliced notation) + if (obs.length >= 3) + obsIR.add(obs[2]); //Name + if (obs.length >= 4) + obsIR.add(obs[3]); //Metadata + obstaclesIR.add(obsIR); + + + String[] xPos = obs[0].split("\\.\\.\\."); double startX; @@ -342,6 +385,17 @@ else if (t.length >= 2) for (String s: tanks) { String[] tank = s.split("-"); + + ArrayList tankIR = new ArrayList<>(); + tankIR.add(tank[0]); //X Coordinate + tankIR.add(tank[1]); //Y Coordinate + tankIR.add(tank[2]); //Name + if (tank.length >= 4) + tankIR.add(tank[3]); //Angle + if (tank.length >= 5) + tankIR.add(tank[4]); //Team + tanksIR.add(tankIR); + double x = Game.tile_size * (0.5 + Double.parseDouble(tank[0])); double y = Game.tile_size * (0.5 + Double.parseDouble(tank[1])); String type = tank[2].toLowerCase(); @@ -435,6 +489,160 @@ else if (t.length >= 2) } } + public void init() + { + init(false); + } + + public void init(boolean remote) + { + init(new ArrayList<>(), remote, ScreenPartyHost.isServer && Game.disablePartyFriendlyFire); + } + + public void init(ArrayList customTanks) + { + init(customTanks, false, ScreenPartyHost.isServer && Game.disablePartyFriendlyFire); + } + + public void init(ArrayList customTanks, boolean remote, boolean disableFriendlyFire) + { + this.remote = remote; + this.disableFriendlyFire = disableFriendlyFire; + this.customTanks.addAll(customTanks); + + if (teamsMap.isEmpty()) + { + if (disableFriendlyFire) + { + teamsMap.put("ally", Game.playerTeamNoFF); + teamsMap.put("enemy", Game.enemyTeamNoFF); + } + else + { + teamsMap.put("ally", Game.playerTeam); + teamsMap.put("enemy", Game.enemyTeam); + } + } + + for (ArrayList obs: obstaclesIR) + { + String[] xs = obs.get(0).split(":"); + double startX = Double.parseDouble(xs[0]); + double endX = (xs.length > 1 ? Double.parseDouble(xs[1]) : startX) + 1; + String[] ys = obs.get(1).split(":"); + double startY = Double.parseDouble(ys[0]); + double endY = (ys.length > 1 ? Double.parseDouble(ys[1]) : startY) + 1; + + for (double x = startX; x < endX; x++) + { + for (double y = startY; y < endY; y++) + { + Obstacle o = Game.registryObstacle.getEntry(obs.get(2)).getObstacle(x, y); + + if (obs.size() >= 4) + o.setMetadata(obs.get(3)); + + if (o instanceof ObstacleBeatBlock) + { + this.synchronizeMusic = true; + this.beatBlocks |= (int) ((ObstacleBeatBlock) o).beatFrequency; + } + + obstacles.add(o); + } + } + } + + int currentCrusadeID = 0; + + LinkedHashMap customTanksMap = new LinkedHashMap<>(); + for (TankAIControlled t: this.customTanks) + customTanksMap.put(t.name, t); + + tanksToRemove = new ArrayList<>(); + + for (ArrayList tankIR: tanksIR) + { + String[] tank = new String[tankIR.size()]; + tankIR.toArray(tank); + double x = Game.tile_size * (0.5 + Double.parseDouble(tank[0])); + double y = Game.tile_size * (0.5 + Double.parseDouble(tank[1])); + String type = tank[2].toLowerCase(); + double angle = 0; + + StringBuilder metadata = new StringBuilder(); + for (int i = 3; i < tank.length; i++) + { + metadata.append(tank[i]); + if (i < tank.length - 1) + metadata.append("-"); + } + + if (tank.length >= 4) + angle = (Math.PI / 2 * Double.parseDouble(tank[3])); + + Team team = Game.enemyTeam; + + if (this.disableFriendlyFire) + team = Game.enemyTeamNoFF; + + if (enableTeams) + { + if (tank.length >= 5) + team = teamsMap.get(tank[4]); + else + team = null; + } + + Tank t; + if (type.equals("player")) + { + if (team == Game.enemyTeam) + team = Game.playerTeam; + + if (team == Game.enemyTeamNoFF) + team = Game.playerTeamNoFF; + + this.playerSpawnsX.add(x); + this.playerSpawnsY.add(y); + this.playerSpawnsAngle.add(angle); + this.playerSpawnsTeam.add(team); + + continue; + } + + if (customTanksMap.get(type) != null) + t = customTanksMap.get(type).instantiate(type, x, y, angle); + else + t = Game.registryTank.getEntry(type).getTank(x, y, angle); + + t.crusadeID = currentCrusadeID; + currentCrusadeID++; + + Level l = Game.currentLevel; + Game.currentLevel = this; + if (Crusade.crusadeMode && !Crusade.currentCrusade.respawnTanks && Crusade.currentCrusade.retry && !Crusade.currentCrusade.livingTankIDs.contains(t.crusadeID)) + tanksToRemove.add(t); + else + t.setMetadata(metadata.toString()); + Game.currentLevel = l; + + if (remote) + this.tanks.add(new TankRemote(t)); + else + { + this.tanks.add(t); + setSolidTank((int) Double.parseDouble(tank[0]), (int) Double.parseDouble(tank[1]), true); + } + } + + if (TankModels.tank != null && playerBuilds.isEmpty()) + { + TankPlayer.ShopTankBuild tp = new TankPlayer.ShopTankBuild(); + playerBuilds.add(tp); + } + } + protected static ArrayList getJsonObjects(String s) { int depth = 0; @@ -442,10 +650,14 @@ protected static ArrayList getJsonObjects(String s) ArrayList out = new ArrayList<>(); for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == '/' && s.charAt(i + 1) == '*') + last = i; + if (s.charAt(i) == '{' || s.charAt(i) == '[') { if (depth == 0) - last = i; + if (i - 2 < 0 || s.charAt(i - 1) != '/' || s.charAt(i - 2) != '*') + last = i; depth++; } @@ -1079,4 +1291,160 @@ else if (!Character.isWhitespace(c)) return out.substring(1); } + + public String save() + { + this.obstaclesIR.clear(); + this.tanksIR.clear(); + ArrayList unmarked = (ArrayList) Game.obstacles.clone(); + String[][][] obstacles = new String[Game.registryObstacle.obstacleEntries.size()][this.sizeX][this.sizeY]; + + for (int h = 0; h < Game.registryObstacle.obstacleEntries.size(); h++) + { + for (int i = 0; i < Game.obstacles.size(); i++) + { + Obstacle o = Game.obstacles.get(i); + int x = (int) (o.posX / Game.tile_size); + int y = (int) (o.posY / Game.tile_size); + + if (x < obstacles[h].length && x >= 0 && y < obstacles[h][0].length && y >= 0 && o.name.equals(Game.registryObstacle.getEntry(h).name)) + { + obstacles[h][x][y] = o.getMetadata(); + + unmarked.remove(o); + } + } + + //compression + for (int i = 0; i < this.sizeX; i++) + { + for (int j = 0; j < this.sizeY; j++) + { + if (obstacles[h][i][j] != null) + { + String stack = obstacles[h][i][j]; + + int xLength = 0; + + while (true) + { + xLength += 1; + + if (i + xLength >= obstacles[h].length) + break; + else if (!Objects.equals(obstacles[h][i + xLength][j], stack)) + break; + } + + + int yLength = 0; + + while (true) + { + yLength += 1; + + if (j + yLength >= obstacles[h][0].length) + break; + else if (!Objects.equals(obstacles[h][i][j + yLength], stack)) + break; + } + + String name = ""; + String obsName = Game.registryObstacle.obstacleEntries.get(h).name; + + if (!obsName.equals("normal") || !stack.equals("1.0")) + name = obsName; + + if (xLength >= yLength) + { + ArrayList obs = new ArrayList<>(); + if (xLength == 1) + { + obs.add(Integer.toString(i)); + obs.add(Integer.toString(j)); + obs.add(name); + } + else + { + obs.add(Integer.toString(i) + ":" + Integer.toString(i + xLength - 1)); + obs.add(Integer.toString(j)); + obs.add(name); + } + + if (!stack.isEmpty()) + obs.add(stack); + + this.obstaclesIR.add(obs); + + for (int z = 0; z < xLength; z++) + { + obstacles[h][i + z][j] = null; + } + } + else + { + ArrayList obs = new ArrayList<>(); + obs.add(Integer.toString(i)); + obs.add(Integer.toString(j) + ":" + Integer.toString(j + yLength - 1)); + obs.add(name); + + if (!stack.isEmpty()) + obs.add(stack); + + this.obstaclesIR.add(obs); + + for (int z = 0; z < yLength; z++) + { + obstacles[h][i][j + z] = null; + } + } + } + } + } + } + + for (Obstacle obstacle: unmarked) + { + ArrayList obs = new ArrayList<>(); + obs.add(Integer.toString((int) (obstacle.posX / Game.tile_size))); + obs.add(Integer.toString((int) (obstacle.posY / Game.tile_size))); + obs.add(obstacle.name); + + if (obstacle instanceof ObstacleUnknown && ((ObstacleUnknown) obstacle).metadata != null) + obs.add(((ObstacleUnknown) obstacle).metadata); + else + { + String meta = obstacle.getMetadata(); + if (!meta.isEmpty()) + obs.add(meta); + } + + + this.obstaclesIR.add(obs); + } + + for (int i = 0; i < Game.movables.size(); i++) + { + if (Game.movables.get(i) instanceof Tank) + { + Tank t = (Tank) Game.movables.get(i); + int x = (int) (t.posX / Game.tile_size); + int y = (int) (t.posY / Game.tile_size); + int angle = (int) (t.angle * 2 / Math.PI); + + ArrayList tank = new ArrayList<>(); + tank.add(Integer.toString(x)); + tank.add(Integer.toString(y)); + tank.add(t.name); + tank.add(Integer.toString(angle)); + + if (t.team != null) + tank.add(t.team.name); + + this.tanksIR.add(tank); + } + } + + return Serializer.toTanksON(this); + } } diff --git a/src/main/java/tanks/Team.java b/src/main/java/tanks/Team.java index a5aaea128..16e91159f 100644 --- a/src/main/java/tanks/Team.java +++ b/src/main/java/tanks/Team.java @@ -1,8 +1,7 @@ package tanks; import basewindow.Color; -import tanks.tankson.Property; -import tanks.tankson.TanksONable; +import tanks.tankson.*; @TanksONable("team") public class Team diff --git a/src/main/java/tanks/gui/screen/DisplayCrusadeLevels.java b/src/main/java/tanks/gui/screen/DisplayCrusadeLevels.java index ec769221f..ced9dd183 100644 --- a/src/main/java/tanks/gui/screen/DisplayCrusadeLevels.java +++ b/src/main/java/tanks/gui/screen/DisplayCrusadeLevels.java @@ -7,6 +7,7 @@ import tanks.rendering.TerrainRenderer; import tanks.tank.TankAIControlled; import tanks.tank.TankSpawnMarker; +import tanks.tankson.Serializer; import java.util.ArrayList; import java.util.HashMap; @@ -89,7 +90,16 @@ public void initialize(ScreenLevel l) Game.cleanUp(); - l.level = new Level(l.levelString, l.tanks); + try + { + l.level = (Level) Serializer.fromTanksON(l.levelString); + l.level.init(l.tanks); + l.level.levelString = l.levelString; + } + catch (RuntimeException e) + { + l.level = new Level(l.levelString, l.tanks); + } if (!l.isTransition) addTransitionLevels(l); diff --git a/src/main/java/tanks/gui/screen/ScreenCrusadeAddLevel.java b/src/main/java/tanks/gui/screen/ScreenCrusadeAddLevel.java index 81ce6a4a1..1f4348e99 100644 --- a/src/main/java/tanks/gui/screen/ScreenCrusadeAddLevel.java +++ b/src/main/java/tanks/gui/screen/ScreenCrusadeAddLevel.java @@ -4,6 +4,7 @@ import tanks.Game; import tanks.gui.Button; import tanks.gui.SavedFilesList; +import tanks.tankson.Serializer; public class ScreenCrusadeAddLevel extends ScreenPlaySavedLevels { @@ -34,7 +35,14 @@ public void initializeLevels() if (Game.loadLevel(file, s)) { s.level.buildOverrides.addAll(Game.currentLevel.playerBuilds); - s.level.levelString = Game.currentLevel.levelString; + try + { + s.level.levelString = Serializer.toTanksON(Serializer.fromTanksON(Game.currentLevel.levelString)); + } + catch (RuntimeException e) + { + s.level.levelString = Game.currentLevel.levelString; + } s.level.tanks.addAll(Game.currentLevel.customTanks); Game.screen = s; } diff --git a/src/main/java/tanks/gui/screen/ScreenCrusadeEditor.java b/src/main/java/tanks/gui/screen/ScreenCrusadeEditor.java index 4e2d82f57..052bd8693 100644 --- a/src/main/java/tanks/gui/screen/ScreenCrusadeEditor.java +++ b/src/main/java/tanks/gui/screen/ScreenCrusadeEditor.java @@ -749,7 +749,20 @@ public void save() for (int i = 0; i < this.crusade.levels.size(); i++) { String l = this.crusade.levels.get(i).levelString; - f.println(l.substring(l.indexOf('{'), l.indexOf('}') + 1) + " name=" + this.crusade.levels.get(i).levelName); + try + { + Map m = (Map) TanksON.parseObject(l); + m.remove("coins"); + m.remove("shop"); + m.remove("items"); + m.remove("builds"); + m.remove("custom_tanks"); + f.println(TanksON.toString(m) + " name=" + this.crusade.levels.get(i).levelName); + } + catch (RuntimeException e) + { + f.println(l.substring(l.indexOf('{'), l.lastIndexOf('}') + 1) + " name=" + this.crusade.levels.get(i).levelName); + } } f.println("build_overrides"); diff --git a/src/main/java/tanks/gui/screen/leveleditor/ScreenLevelEditor.java b/src/main/java/tanks/gui/screen/leveleditor/ScreenLevelEditor.java index cab2df008..7cd3a869a 100644 --- a/src/main/java/tanks/gui/screen/leveleditor/ScreenLevelEditor.java +++ b/src/main/java/tanks/gui/screen/leveleditor/ScreenLevelEditor.java @@ -1737,6 +1737,38 @@ public void save() } public void save(String levelName) + { + this.new_save(levelName); + } + + public void new_save(String levelName) + { + Game.currentLevelString = this.level.save(); + + BaseFile file = Game.game.fileManager.getFile(Game.homedir + Game.levelDir + "/" + levelName); + if (file.exists()) + { + if (!this.level.editable) + { + return; + } + } + + try + { + file.create(); + + file.startWriting(); + file.println(Serializer.toTanksON(this.level)); + file.stopWriting(); + } + catch (IOException e) + { + Game.exitToCrash(e); + } + } + + public void legacy_save(String levelName) { StringBuilder level = new StringBuilder("{"); @@ -2534,7 +2566,7 @@ public void play() this.replaceSpawns(); Game.currentLevel.reloadTiles(); - Game.currentLevel = new Level(Game.currentLevelString); + Game.currentLevel = this.level; Game.currentLevel.tilesRandomSeed = level.tilesRandomSeed; Game.currentLevel.timer = level.timer; diff --git a/src/main/java/tanks/item/Item.java b/src/main/java/tanks/item/Item.java index c04983de6..7868c7a25 100644 --- a/src/main/java/tanks/item/Item.java +++ b/src/main/java/tanks/item/Item.java @@ -253,7 +253,7 @@ public void subtractItem() public static ItemStack fromString(Player p, String s) { - if (!s.startsWith("{")) + if (!s.startsWith("{") && Game.compareVersions(Serializer.getVersion(s), "1.1") < 0) return fromStringLegacy(p, s); ItemStack i = (ItemStack) Serializer.fromTanksON(s); diff --git a/src/main/java/tanks/network/event/EventLoadLevel.java b/src/main/java/tanks/network/event/EventLoadLevel.java index 1346a45d8..a5438c6b9 100644 --- a/src/main/java/tanks/network/event/EventLoadLevel.java +++ b/src/main/java/tanks/network/event/EventLoadLevel.java @@ -8,6 +8,7 @@ import tanks.network.NetworkUtils; import tanks.tank.TankAIControlled; import tanks.tank.TankPlayer; +import tanks.tankson.Serializer; import io.netty.buffer.ByteBuf; @@ -82,7 +83,15 @@ public void execute() if (level.startsWith("minigame=")) Game.currentLevel = Game.registryMinigame.minigames.get(level.substring(level.indexOf("=") + 1)).getConstructor().newInstance(); else - Game.currentLevel = new Level(level, new ArrayList<>(), true, disableFriendlyFire); + try + { + Game.currentLevel = (Level) Serializer.fromTanksON(level); + Game.currentLevel.init(new ArrayList<>(), true, disableFriendlyFire); + } + catch (RuntimeException e) + { + Game.currentLevel = new Level(level, new ArrayList<>(), true, disableFriendlyFire); + } Game.currentLevel.startTime = startTime; Game.currentLevel.loadLevel(); diff --git a/src/main/java/tanks/tankson/Serializer.java b/src/main/java/tanks/tankson/Serializer.java index 80ad4cfb8..8a97303c1 100644 --- a/src/main/java/tanks/tankson/Serializer.java +++ b/src/main/java/tanks/tankson/Serializer.java @@ -1,24 +1,38 @@ package tanks.tankson; import basewindow.Color; -import tanks.Game; -import tanks.Team; +import tanks.*; import tanks.bullet.*; import tanks.item.Item; import tanks.tank.*; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; +import java.lang.reflect.*; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class Serializer { + public static String TANKSON_VERSION = "1.1"; + public static HashMap, Object> defaults = new HashMap<>(); public static HashMap userTanks = new HashMap<>(); + /** Gets the Version present in the TanksON Shebang. If no shebang is present, defaults to 1.0 */ + public static String getVersion(String s) + { + Pattern pattern = Pattern.compile("^/\\*TANKSON v(\\d+\\.\\d+)\\*/"); + Matcher m = pattern.matcher(s); + String version = "1.0"; + if (m.find()) + version = m.group(1); + + return version; + } + public static Class getCorrectClass(Object o) { if (o instanceof TankAIControlled) @@ -138,6 +152,29 @@ else if (o2 instanceof ArrayList) p.put(getid(f), f.get(o)); } } + else if (o2 instanceof Map) + { + if (!((Map) o2).isEmpty() && isTanksONable(((Map) o2).values().iterator().next())) + { + ArrayList o3keys = new ArrayList<>(); + ArrayList o3vals = new ArrayList<>(); + for (Map.Entry o3: ((Map) o2).entrySet()) + { + if (o3.getKey() instanceof Byte || o3.getKey() instanceof Character || o3.getKey() instanceof String || o3.getKey() instanceof Number || + o3.getKey() instanceof Boolean) + { + o3keys.add(o3.getKey()); + o3vals.add(toMap(o3.getValue())); + } + else + throw new RuntimeException("Key must be Primitive or String for Map Serialization. Type: " + o3.getKey().getClass()); + } + p.put(getid(f), Arrays.asList(o3keys, o3vals)); + + } + else + p.put(getid(f), f.get(o)); + } else if (o2 instanceof Enum) p.put(getid(f), ((Enum) o2).name()); else if (o2 instanceof Serializable) @@ -158,16 +195,25 @@ else if (o2 instanceof Serializable) public static String toTanksON(Object o) { - return TanksON.toString(toMap(o)); + String shebang = "/*TANKSON v" + TANKSON_VERSION + "*/"; + return shebang + TanksON.toString(toMap(o)); } public static Object fromTanksON(String s) { - Object o = TanksON.parseObject(s); - if (o instanceof Map) - return parseObject((Map) o); + + if (Game.compareVersions(getVersion(s), TANKSON_VERSION) <= 0) + { + Object o = TanksON.parseObject(s); + if (o instanceof Map) + return parseObject((Map) o); + else + throw new RuntimeException("Unexpected type of object: " + o.toString()); + } else - throw new RuntimeException("Unexpected type of object: " + o.toString()); + { + throw new RuntimeException("Unknown TanksON Version " + getVersion(s) + ". You may be running an older version of Tanks with a newer game file."); + } } public static boolean equivalent(Object a, Object b) @@ -348,6 +394,9 @@ public static Object parseObject(Map m) case "team": o = new Team(); break; + case "level": + o = new Level(); + break; default: throw new RuntimeException("Bad object type: " + (String) m.get("obj_type")); } @@ -378,8 +427,16 @@ public static Object parseObject(Map m) else if (o2 instanceof ArrayList) { ParameterizedType pt = (ParameterizedType) f.getGenericType(); + Type t = pt.getActualTypeArguments()[0]; + Class elem; + if (t instanceof Class) + elem = (Class) t; + else if (t instanceof ParameterizedType) + elem = (Class) ((ParameterizedType) t).getRawType(); + else + elem = (Class) t; ArrayList arr = (ArrayList) m.get(getid(f)); - if (!arr.isEmpty() && (arr.get(0) instanceof Map)) + if (!arr.isEmpty() && isTanksONable(elem)) { ArrayList o3s = new ArrayList(); for (Map o3: ((ArrayList) m.get(getid(f)))) @@ -388,7 +445,7 @@ else if (o2 instanceof ArrayList) } f.set(o, o3s); } - else if (pt.getActualTypeArguments()[0] == Color.class) + else if (elem == Color.class) { ArrayList colors = new ArrayList<>(); ArrayList> els = (ArrayList>) m.get(getid(f)); @@ -403,6 +460,27 @@ else if (pt.getActualTypeArguments()[0] == Color.class) f.set(o, m.get(getid(f))); + } + else if (o2 instanceof Map) + { + ArrayList keys = (ArrayList) ((ArrayList) m.get(getid(f))).get(0); + ArrayList vals = (ArrayList) ((ArrayList) m.get(getid(f))).get(1); + ParameterizedType pt = (ParameterizedType) f.getGenericType(); + Map o3s; + if (o2 instanceof LinkedHashMap) + o3s = new LinkedHashMap<>(); + else if (o2 instanceof HashMap) + o3s = new HashMap<>(); + else + throw new RuntimeException("Unknown map type: " + o2.getClass()); + for (int i = 0; i < keys.size(); i++) + { + if (isTanksONable((Class) pt.getActualTypeArguments()[1])) + ((Map) o3s).put(keys.get(i), parseObject((Map) vals.get(i))); + else + ((Map) o3s).put(keys.get(i), vals.get(i)); + } + f.set(o, o3s); } else if (o2 instanceof Color) { @@ -441,7 +519,7 @@ else if (o2 instanceof Boolean) } catch (Exception e) { - System.out.println(getid(f)); + System.err.println(getid(f)); throw new RuntimeException(e); } } @@ -475,7 +553,7 @@ else if (o2 instanceof Boolean) } catch (NoSuchFieldException | NullPointerException | IllegalAccessException e) { - System.out.println("Unconvertable field found! " + k); + System.err.println("Unconvertable field found! " + k); e.printStackTrace(); } }