From: David Polakovic Date: Tue, 3 Nov 2026 15:06:11 +0000 (+0100) Subject: feat: Custom game types extraction implemented X-Git-Tag: 0.3.0 X-Git-Url: https://git.dpolakovic.space/%22%22.esc_url%28%24base_url%29.%22//%22%24link/%22/%22%22.esc_url%28%24base_url%29.%22//%22%24link/%22?a=commitdiff_plain;p=halo3-reclaimer feat: Custom game types extraction implemented -new class GameModeReclaimer.java -updated documentation, README and build.xml for version changes -changed Error numbering --- diff --git a/README.txt b/README.txt index 2b6ef61..1d3ec49 100755 --- a/README.txt +++ b/README.txt @@ -8,26 +8,27 @@ ABOUT: Today it is almost impossible to get you player data like screenshots or Forge map variants from classic Xbox 360 Halo 3 to you PC. This program solves this -problem. The reclaimer currently exports: +problem. The Reclaimer currently exports: - player screenshots - Forge map variants +- Custom game types BUILDING FORM SOURCE: - Even tough I am not a supporter of distributing software + Even though I am not a supporter of distributing software in already built form, I do so for the comfort of target audience of this specific program. So unless you want to change some suff in the code, YOU CAN SKIP THIS PROCEDURE. Otherwise, there is a build.xml file configured for your - (and mine) convinience which requires build tool Apache Ant. + (and mine) convinience, which requires build tool Apache Ant. https://ant.apache.org/index.html To build the program: 1. Install Ant 2. Inspect the 'build.xml' in root directory - 3. Inspect the code in src to see what it does + 3. Inspect the code in src/ to see what it does 4. Then simply run: ant all @@ -49,7 +50,7 @@ HOW TO RUN: HOW TO USE - See user manual in 'Doc/user-manual.pdf'. + See user manual in doc/user-manual.pdf. @@ -73,3 +74,10 @@ CONTACT: RELEASE NOTES: 0.1 - Screenshots extracting 0.2 - Forge maps extracting + 0.3 - Custom game types extracting + + + +TO DO: + 1.0 - Films extracting + 1.? - Campaign saves extracting diff --git a/build.xml b/build.xml index 820bf86..698ca18 100644 --- a/build.xml +++ b/build.xml @@ -73,8 +73,8 @@ version="true" use="true" windowtitle="Halo 3 Reclaimer" - doctitle="Halo 3 Reclaimer - File extractor from Xbox 360 version of Halo 3" - bottom="Copyright 2025. Documentation licensed under GFDL v1.3." + doctitle="Halo 3 Reclaimer - player data extractor from Xbox 360 version of Halo 3" + bottom="Copyright 2025, 2026. Documentation licensed under GFDL v1.3." overview="overview.html"> @@ -90,4 +90,4 @@ - \ No newline at end of file + diff --git a/build/Arbiter$Kind.class b/build/Arbiter$Kind.class index 90195c3..13e877d 100644 Binary files a/build/Arbiter$Kind.class and b/build/Arbiter$Kind.class differ diff --git a/build/Arbiter.class b/build/Arbiter.class index 17cab3c..dc7d0b0 100644 Binary files a/build/Arbiter.class and b/build/Arbiter.class differ diff --git a/build/Cortana.class b/build/Cortana.class index 54caa78..8513d64 100644 Binary files a/build/Cortana.class and b/build/Cortana.class differ diff --git a/build/ErrorHandler.class b/build/ErrorHandler.class index e0c5fce..365a24f 100644 Binary files a/build/ErrorHandler.class and b/build/ErrorHandler.class differ diff --git a/build/GameModeReclaimer.class b/build/GameModeReclaimer.class new file mode 100644 index 0000000..d874a20 Binary files /dev/null and b/build/GameModeReclaimer.class differ diff --git a/build/GraphicalUI$BrowseRawActionListener$1.class b/build/GraphicalUI$BrowseRawActionListener$1.class index 11b2217..0c1173c 100644 Binary files a/build/GraphicalUI$BrowseRawActionListener$1.class and b/build/GraphicalUI$BrowseRawActionListener$1.class differ diff --git a/build/GraphicalUI$BrowseRawActionListener.class b/build/GraphicalUI$BrowseRawActionListener.class index 5245384..c092a72 100644 Binary files a/build/GraphicalUI$BrowseRawActionListener.class and b/build/GraphicalUI$BrowseRawActionListener.class differ diff --git a/build/GraphicalUI$BrowseSaveActionListener.class b/build/GraphicalUI$BrowseSaveActionListener.class index 18637bf..7e9f61b 100644 Binary files a/build/GraphicalUI$BrowseSaveActionListener.class and b/build/GraphicalUI$BrowseSaveActionListener.class differ diff --git a/build/GraphicalUI$ReclaimActionListener$1.class b/build/GraphicalUI$ReclaimActionListener$1.class index db8c726..e2c5b56 100644 Binary files a/build/GraphicalUI$ReclaimActionListener$1.class and b/build/GraphicalUI$ReclaimActionListener$1.class differ diff --git a/build/GraphicalUI$ReclaimActionListener.class b/build/GraphicalUI$ReclaimActionListener.class index 8756b54..6613277 100644 Binary files a/build/GraphicalUI$ReclaimActionListener.class and b/build/GraphicalUI$ReclaimActionListener.class differ diff --git a/build/GraphicalUI.class b/build/GraphicalUI.class index 4203d4e..2dc9522 100644 Binary files a/build/GraphicalUI.class and b/build/GraphicalUI.class differ diff --git a/build/MapReclaimer$1.class b/build/MapReclaimer$1.class deleted file mode 100644 index 7c73151..0000000 Binary files a/build/MapReclaimer$1.class and /dev/null differ diff --git a/build/MapReclaimer$ContentInfo.class b/build/MapReclaimer$ContentInfo.class new file mode 100644 index 0000000..a292b1f Binary files /dev/null and b/build/MapReclaimer$ContentInfo.class differ diff --git a/build/MapReclaimer$MapInfo.class b/build/MapReclaimer$MapInfo.class deleted file mode 100644 index 677ce76..0000000 Binary files a/build/MapReclaimer$MapInfo.class and /dev/null differ diff --git a/build/MapReclaimer$StfsEntry.class b/build/MapReclaimer$StfsEntry.class index a238425..4e78972 100644 Binary files a/build/MapReclaimer$StfsEntry.class and b/build/MapReclaimer$StfsEntry.class differ diff --git a/build/MapReclaimer$StfsPackage.class b/build/MapReclaimer$StfsPackage.class index 12c5e3a..93e7c2c 100644 Binary files a/build/MapReclaimer$StfsPackage.class and b/build/MapReclaimer$StfsPackage.class differ diff --git a/build/MapReclaimer.class b/build/MapReclaimer.class index 65cfeb1..8c72a47 100644 Binary files a/build/MapReclaimer.class and b/build/MapReclaimer.class differ diff --git a/build/ScreenshotReclaimer.class b/build/ScreenshotReclaimer.class index dff2b10..8d1c3c1 100644 Binary files a/build/ScreenshotReclaimer.class and b/build/ScreenshotReclaimer.class differ diff --git a/build/TerminalUI.class b/build/TerminalUI.class index 602d2c3..69b0969 100644 Binary files a/build/TerminalUI.class and b/build/TerminalUI.class differ diff --git a/doc/Arbiter.html b/doc/Arbiter.html index 0e1d10d..7e2d7e4 100644 --- a/doc/Arbiter.html +++ b/doc/Arbiter.html @@ -2,9 +2,9 @@ - + Arbiter (Halo 3 Reclaimer) - + @@ -118,18 +118,20 @@ extends java.lang.Object Cortana first, which copies the Halo 3 files out of the Xbox file system; other files on the stick are ignored then.

-

Halo 3 keeps every screenshot and every Forge map in its own package, - so the Arbiter looks inside each file: a package holding +

Halo 3 keeps every screenshot, Forge map and custom game type in its own + package, so the Arbiter looks inside each file: a package holding screen.shot is a screenshot, one holding sandbox.map - is a map. Screenshots are checked first, then maps. Anything else is skipped. - Only when the content says nothing (DDS, TGA, raw textures) the file name - decides, like in version 1.0.

+ is a map and one holding variant.slayer, variant.ctf... + is a game mode. Screenshots are checked first, then maps, then game modes. + Anything else is skipped. Only when the content says nothing (DDS, TGA, + raw textures) the file name decides, like in version 1.0.

-

Results go to two folders in the output directory: - screenshots-reclaimed and maps-reclaimed.

+

Results go to halo-3-reclaimed-files in the output directory, + sorted into screenshots, maps and game-modes. + Files are named after their in-game names, see saveFiles(java.io.File, java.lang.String, java.lang.String, java.util.Map<java.lang.String, byte[]>).

Version:
-
1.0
+
1.1
@@ -151,8 +153,16 @@ extends java.lang.Object static java.lang.String +GAME_MODES_DIR  + + +static java.lang.String MAPS_DIR  + +static java.lang.String +OUTPUT_DIR  + static java.lang.String SCREENSHOTS_DIR  @@ -193,7 +203,7 @@ extends java.lang.Object static java.lang.String processFile(java.io.File input, java.io.File outputDir) -
Reclaims all screenshots and maps found in a file or folder.
+
Reclaims all screenshots, maps and game modes found in a file or folder.
@@ -218,6 +228,19 @@ extends java.lang.Object

Field Detail

+ + + +
    +
  • +

    OUTPUT_DIR

    +
    public static final java.lang.String OUTPUT_DIR
    +
    +
    See Also:
    +
    Constant Field Values
    +
    +
  • +
@@ -234,7 +257,7 @@ extends java.lang.Object -
    +
    • MAPS_DIR

      public static final java.lang.String MAPS_DIR
      @@ -244,6 +267,19 @@ extends java.lang.Object
    + + + +
      +
    • +

      GAME_MODES_DIR

      +
      public static final java.lang.String GAME_MODES_DIR
      +
      +
      See Also:
      +
      Constant Field Values
      +
      +
    • +
@@ -278,10 +314,10 @@ extends java.lang.Object
public static java.lang.String processFile(java.io.File input,
                                            java.io.File outputDir)
                                     throws java.lang.Exception
-
Reclaims all screenshots and maps found in a file or folder.
+
Reclaims all screenshots, maps and game modes found in a file or folder.
Returns:
-
what was reclaimed, for example "Reclaimed 2 pictures and 1 map"
+
what was reclaimed, for example "Reclaimed 2 pictures, 1 map and 3 game modes"
Throws:
java.lang.Exception - if nothing could be reclaimed
@@ -357,6 +393,6 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/Cortana.html b/doc/Cortana.html index 65665a5..14b9597 100644 --- a/doc/Cortana.html +++ b/doc/Cortana.html @@ -2,9 +2,9 @@ - + Cortana (Halo 3 Reclaimer) - + @@ -331,6 +331,6 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/ErrorHandler.html b/doc/ErrorHandler.html index 6ec8e75..a5a2319 100644 --- a/doc/ErrorHandler.html +++ b/doc/ErrorHandler.html @@ -2,9 +2,9 @@ - + ErrorHandler (Halo 3 Reclaimer) - + @@ -51,7 +51,7 @@ var activeTableTab = "activeTableTab";
Version:
-
1.0
+
1.1
@@ -431,7 +431,7 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/GameModeReclaimer.html b/doc/GameModeReclaimer.html new file mode 100644 index 0000000..9fd2a79 --- /dev/null +++ b/doc/GameModeReclaimer.html @@ -0,0 +1,312 @@ + + + + + +GameModeReclaimer (Halo 3 Reclaimer) + + + + + + + + +
+ + + + + + + +
Halo 3 Reclaimer
+
+ + + +
+

Class GameModeReclaimer

+
+
+
    +
  • java.lang.Object
  • +
  • +
      +
    • GameModeReclaimer
    • +
    +
  • +
+
+
    +
  • +
    +
    +
    public class GameModeReclaimer
    +extends java.lang.Object
    +
    Core extraction engine for custom game types (game modes). + +

    Halo 3 saves every custom game type as its own Xbox 360 package, just + like the Forge maps. The package holds one file named after the game's + rules, for example variant.slayer or variant.ctf. + That file is the game mode itself, all of its settings, stored in Bungie's + BLF format. The package and BLF readers are shared with MapReclaimer.

    + +

    Supported input formats:

    +
      +
    • STFS container files holding a Halo 3 game type
    • +
    • already extracted variant.* files (BLF)
    • +
    + +

    Two files are saved, named after the game mode (see Arbiter.saveFiles):

    +
      +
    • team_snipers.slayer: byte for byte the file the console + keeps inside the package, its ending tells the game type
    • +
    • team_snipers.package: the untouched package, which tools + like Horizon or Velocity can sign for another player's profile + (only when the input was a package)
    • +
    +
    +
    Version:
    +
    1.0
    +
    +
  • +
+
+
+
    +
  • + +
      +
    • + + +

      Constructor Summary

      + + + + + + + + +
      Constructors 
      Constructor and Description
      GameModeReclaimer() 
      +
    • +
    + +
      +
    • + + +

      Method Summary

      + + + + + + + + + + +
      All Methods Instance Methods Concrete Methods 
      Modifier and TypeMethod and Description
      voidextractGameMode(java.io.File inputFile, + java.io.File outputDir) +
      Extracts the game mode from an Xbox 360 file and saves it, together with + the package it came in, under its in-game name in the output directory.
      +
      +
        +
      • + + +

        Methods inherited from class java.lang.Object

        +clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
      • +
      +
    • +
    +
  • +
+
+
+
    +
  • + +
      +
    • + + +

      Constructor Detail

      + + + +
        +
      • +

        GameModeReclaimer

        +
        public GameModeReclaimer()
        +
      • +
      +
    • +
    + +
      +
    • + + +

      Method Detail

      + + + +
        +
      • +

        extractGameMode

        +
        public void extractGameMode(java.io.File inputFile,
        +                            java.io.File outputDir)
        +                     throws java.lang.Exception
        +
        Extracts the game mode from an Xbox 360 file and saves it, together with + the package it came in, under its in-game name in the output directory.
        +
        +
        Throws:
        +
        java.lang.Exception - if the file cannot be read, is not a Halo 3 game mode, + or is damaged
        +
        +
      • +
      +
    • +
    +
  • +
+
+
+ + +
+ + + + + + + +
File extractor from Xbox 360 version of Halo 3
+
+ + +

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

+ + diff --git a/doc/GraphicalUI.html b/doc/GraphicalUI.html index 94f7b5d..25d08c7 100644 --- a/doc/GraphicalUI.html +++ b/doc/GraphicalUI.html @@ -2,9 +2,9 @@ - + GraphicalUI (Halo 3 Reclaimer) - + @@ -50,7 +50,7 @@ var activeTableTab = "activeTableTab";
Version:
-
1.1
+
1.2
See Also:
Serialized Form
@@ -400,7 +400,7 @@ extends javax.swing.JFrame -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/Main.html b/doc/Main.html index 711b5d2..a357656 100644 --- a/doc/Main.html +++ b/doc/Main.html @@ -2,9 +2,9 @@ - + Main (Halo 3 Reclaimer) - + @@ -276,6 +276,6 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/MapReclaimer.html b/doc/MapReclaimer.html index e50c908..28cafee 100644 --- a/doc/MapReclaimer.html +++ b/doc/MapReclaimer.html @@ -2,9 +2,9 @@ - + MapReclaimer (Halo 3 Reclaimer) - + @@ -123,7 +123,8 @@ extends java.lang.Object

Unlike screenshots, the map has no magic bytes worth scanning for, so this class reads the STFS file system properly: it walks the file table, follows the block chain of sandbox.map and checks - each block against the package hash tables.

+ each block against the package hash tables. These package and BLF readers + are shared with the other reclaimers.

Supported input formats:

    @@ -131,11 +132,17 @@ extends java.lang.Object
  • already extracted sandbox.map files (BLF)
-

The saved file is not converted in any way. It is byte for byte the - sandbox.map the console keeps inside the package.

+

Two files are saved, named after the map (see Arbiter.saveFiles):

+
    +
  • guardian_infection.map: byte for byte the + sandbox.map the console keeps inside the package
  • +
  • guardian_infection.package: the untouched package, which + tools like Horizon or Velocity can sign for another player's profile + (only when the input was a package)
  • +
Version:
-
1.0
+
1.1
@@ -176,8 +183,8 @@ extends java.lang.Object void extractMap(java.io.File inputFile, java.io.File outputDir) -
Extracts the Forge map from an Xbox 360 file and saves it as - <input name>-reclaimed.map in the output directory.
+
Extracts the Forge map from an Xbox 360 file and saves it, together with + the package it came in, under the map's in-game name in the output directory.
@@ -228,8 +235,8 @@ extends java.lang.Object
public void extractMap(java.io.File inputFile,
                        java.io.File outputDir)
                 throws java.lang.Exception
-
Extracts the Forge map from an Xbox 360 file and saves it as - <input name>-reclaimed.map in the output directory.
+
Extracts the Forge map from an Xbox 360 file and saves it, together with + the package it came in, under the map's in-game name in the output directory.
Throws:
java.lang.Exception - if the file cannot be read, is not a Halo 3 Forge map, @@ -307,6 +314,6 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/ScreenshotReclaimer.html b/doc/ScreenshotReclaimer.html index f5d6983..a526616 100644 --- a/doc/ScreenshotReclaimer.html +++ b/doc/ScreenshotReclaimer.html @@ -2,9 +2,9 @@ - + ScreenshotReclaimer (Halo 3 Reclaimer) - + @@ -133,7 +133,7 @@ extends java.lang.Object
Version:
-
1.0
+
1.1
@@ -302,6 +302,6 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/TerminalUI.html b/doc/TerminalUI.html index ebfd352..6cc1698 100644 --- a/doc/TerminalUI.html +++ b/doc/TerminalUI.html @@ -2,9 +2,9 @@ - + TerminalUI (Halo 3 Reclaimer) - + @@ -116,7 +116,7 @@ extends java.lang.Object

This class contains all the command-line interface logic.

Version:
-
1.0
+
1.2
@@ -330,6 +330,6 @@ extends java.lang.Object -

Copyright 2025. Documentation licensed under GFDL v1.3.

+

Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

diff --git a/doc/allclasses-frame.html b/doc/allclasses-frame.html index 9b46ad1..e5815c1 100644 --- a/doc/allclasses-frame.html +++ b/doc/allclasses-frame.html @@ -2,9 +2,9 @@ - + All Classes (Halo 3 Reclaimer) - + @@ -15,6 +15,7 @@
  • Arbiter
  • Cortana
  • ErrorHandler
  • +
  • GameModeReclaimer
  • GraphicalUI
  • Main
  • MapReclaimer
  • diff --git a/doc/allclasses-noframe.html b/doc/allclasses-noframe.html index 5f89623..6a96ca8 100644 --- a/doc/allclasses-noframe.html +++ b/doc/allclasses-noframe.html @@ -2,9 +2,9 @@ - + All Classes (Halo 3 Reclaimer) - + @@ -15,6 +15,7 @@
  • Arbiter
  • Cortana
  • ErrorHandler
  • +
  • GameModeReclaimer
  • GraphicalUI
  • Main
  • MapReclaimer
  • diff --git a/doc/class-use/Arbiter.html b/doc/class-use/Arbiter.html index 98b442a..fcc82bc 100644 --- a/doc/class-use/Arbiter.html +++ b/doc/class-use/Arbiter.html @@ -2,9 +2,9 @@ - + Uses of Class Arbiter (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/Cortana.html b/doc/class-use/Cortana.html index cc96b38..ec08176 100644 --- a/doc/class-use/Cortana.html +++ b/doc/class-use/Cortana.html @@ -2,9 +2,9 @@ - + Uses of Class Cortana (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/ErrorHandler.html b/doc/class-use/ErrorHandler.html index 64136f5..b473785 100644 --- a/doc/class-use/ErrorHandler.html +++ b/doc/class-use/ErrorHandler.html @@ -2,9 +2,9 @@ - + Uses of Class ErrorHandler (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/GameModeReclaimer.html b/doc/class-use/GameModeReclaimer.html new file mode 100644 index 0000000..a8e7b96 --- /dev/null +++ b/doc/class-use/GameModeReclaimer.html @@ -0,0 +1,127 @@ + + + + + +Uses of Class GameModeReclaimer (Halo 3 Reclaimer) + + + + + + + + + + + +
    +

    Uses of Class
    GameModeReclaimer

    +
    +
    No usage of GameModeReclaimer
    + +
    + + + + + + + +
    File extractor from Xbox 360 version of Halo 3
    +
    + + +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    + + diff --git a/doc/class-use/GraphicalUI.html b/doc/class-use/GraphicalUI.html index afbd7ef..e8f5fa2 100644 --- a/doc/class-use/GraphicalUI.html +++ b/doc/class-use/GraphicalUI.html @@ -2,9 +2,9 @@ - + Uses of Class GraphicalUI (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/Main.html b/doc/class-use/Main.html index 06f5dca..e8e451b 100644 --- a/doc/class-use/Main.html +++ b/doc/class-use/Main.html @@ -2,9 +2,9 @@ - + Uses of Class Main (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/MapReclaimer.html b/doc/class-use/MapReclaimer.html index 90cef78..238ac0b 100644 --- a/doc/class-use/MapReclaimer.html +++ b/doc/class-use/MapReclaimer.html @@ -2,9 +2,9 @@ - + Uses of Class MapReclaimer (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/ScreenshotReclaimer.html b/doc/class-use/ScreenshotReclaimer.html index 94a40f1..57036fc 100644 --- a/doc/class-use/ScreenshotReclaimer.html +++ b/doc/class-use/ScreenshotReclaimer.html @@ -2,9 +2,9 @@ - + Uses of Class ScreenshotReclaimer (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/class-use/TerminalUI.html b/doc/class-use/TerminalUI.html index 75278bd..784c1ee 100644 --- a/doc/class-use/TerminalUI.html +++ b/doc/class-use/TerminalUI.html @@ -2,9 +2,9 @@ - + Uses of Class TerminalUI (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/constant-values.html b/doc/constant-values.html index ea15a6e..ed9bdab 100644 --- a/doc/constant-values.html +++ b/doc/constant-values.html @@ -2,9 +2,9 @@ - + Constant Field Values (Halo 3 Reclaimer) - + @@ -92,18 +92,32 @@ + + +public static final java.lang.String +GAME_MODES_DIR +"game-modes" + + public static final java.lang.String MAPS_DIR -"maps-reclaimed" +"maps" + + + + +public static final java.lang.String +OUTPUT_DIR +"halo-3-reclaimed-files" public static final java.lang.String SCREENSHOTS_DIR -"screenshots-reclaimed" +"screenshots" @@ -129,7 +143,7 @@ public static final java.lang.String BAD_OUTPUT -"123: Output directory problem" +"124: Output directory problem" @@ -226,6 +240,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/deprecated-list.html b/doc/deprecated-list.html index dd1a2bd..3eeed72 100644 --- a/doc/deprecated-list.html +++ b/doc/deprecated-list.html @@ -2,9 +2,9 @@ - + Deprecated List (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/help-doc.html b/doc/help-doc.html index 1bde3bc..55ff8af 100644 --- a/doc/help-doc.html +++ b/doc/help-doc.html @@ -2,9 +2,9 @@ - + API Help (Halo 3 Reclaimer) - + @@ -227,6 +227,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/index-all.html b/doc/index-all.html index 447e6e5..55c380a 100644 --- a/doc/index-all.html +++ b/doc/index-all.html @@ -2,9 +2,9 @@ - + Index (Halo 3 Reclaimer) - + @@ -70,7 +70,7 @@ -
    A B C D E F G M N P S T U  +
    A B C D E F G M N O P S T U 

    A

    @@ -140,10 +140,15 @@
    TUI only: print error AND QUIT
    +
    extractGameMode(File, File) - Method in class GameModeReclaimer
    +
    +
    Extracts the game mode from an Xbox 360 file and saves it, together with + the package it came in, under its in-game name in the output directory.
    +
    extractMap(File, File) - Method in class MapReclaimer
    -
    Extracts the Forge map from an Xbox 360 file and saves it as - <input name>-reclaimed.map in the output directory.
    +
    Extracts the Forge map from an Xbox 360 file and saves it, together with + the package it came in, under the map's in-game name in the output directory.
    extractScreenshot(File, File) - Method in class ScreenshotReclaimer
    @@ -166,6 +171,14 @@

    G

    +
    GAME_MODES_DIR - Static variable in class Arbiter
    +
     
    +
    GameModeReclaimer - Class in <Unnamed>
    +
    +
    Core extraction engine for custom game types (game modes).
    +
    +
    GameModeReclaimer() - Constructor for class GameModeReclaimer
    +
     
    GraphicalUI - Class in <Unnamed>
    Graphical User Interface for Halo 3 Reclaimer.
    @@ -215,6 +228,14 @@
    NOT_READY - Static variable in class ErrorHandler
     
    + + + +

    O

    +
    +
    OUTPUT_DIR - Static variable in class Arbiter
    +
     
    +
    @@ -222,7 +243,7 @@
    processFile(File, File) - Static method in class Arbiter
    -
    Reclaims all screenshots and maps found in a file or folder.
    +
    Reclaims all screenshots, maps and game modes found in a file or folder.
    processFile(String[]) - Static method in class TerminalUI
    @@ -275,7 +296,7 @@
    UNKNOWN_FILE - Static variable in class ErrorHandler
     
    -A B C D E F G M N P S T U  +A B C D E F G M N O P S T U  -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/index.html b/doc/index.html index 37a8f0c..c7527a9 100644 --- a/doc/index.html +++ b/doc/index.html @@ -2,7 +2,7 @@ - + Halo 3 Reclaimer @@ -71,7 +71,7 @@
    -

    Halo 3 Reclaimer - File extractor from Xbox 360 version of Halo 3

    +

    Halo 3 Reclaimer - player data extractor from Xbox 360 version of Halo 3

    @@ -130,6 +130,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/overview-tree.html b/doc/overview-tree.html index e0bc245..614e4b4 100644 --- a/doc/overview-tree.html +++ b/doc/overview-tree.html @@ -2,9 +2,9 @@ - +Class Hierarchy (Halo 3 Reclaimer) - + @@ -102,6 +102,7 @@
  • Cortana
  • ErrorHandler
  • +
  • GameModeReclaimer
  • Main
  • MapReclaimer
  • ScreenshotReclaimer
  • @@ -158,6 +159,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/package-frame.html b/doc/package-frame.html index 962c54b..218ed58 100644 --- a/doc/package-frame.html +++ b/doc/package-frame.html @@ -2,9 +2,9 @@ - +<Unnamed> (Halo 3 Reclaimer) - + @@ -16,6 +16,7 @@
  • Arbiter
  • Cortana
  • ErrorHandler
  • +
  • GameModeReclaimer
  • GraphicalUI
  • Main
  • MapReclaimer
  • diff --git a/doc/package-summary.html b/doc/package-summary.html index e8c9a87..6bc8ce1 100644 --- a/doc/package-summary.html +++ b/doc/package-summary.html @@ -2,9 +2,9 @@ - + (Halo 3 Reclaimer) - + @@ -102,30 +102,36 @@ + + + + - + - + - + - +
    GameModeReclaimer +
    Core extraction engine for custom game types (game modes).
    +
    GraphicalUI
    Graphical User Interface for Halo 3 Reclaimer.
    Main
    Main entry point for the program.
    MapReclaimer
    Core extraction engine for Forge map variants.
    ScreenshotReclaimer
    Core extraction engine for screenshots.
    TerminalUI
    Terminal User Interface for Halo 3 Reclaimer.
    @@ -184,6 +190,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/package-tree.html b/doc/package-tree.html index e1b8c80..846fb69 100644 --- a/doc/package-tree.html +++ b/doc/package-tree.html @@ -2,9 +2,9 @@ - + Class Hierarchy (Halo 3 Reclaimer) - + @@ -102,6 +102,7 @@
  • Cortana
  • ErrorHandler
  • +
  • GameModeReclaimer
  • Main
  • MapReclaimer
  • ScreenshotReclaimer
  • @@ -158,6 +159,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/package-use.html b/doc/package-use.html index 72967b0..970a980 100644 --- a/doc/package-use.html +++ b/doc/package-use.html @@ -2,9 +2,9 @@ - + Uses of Package (Halo 3 Reclaimer) - + @@ -122,6 +122,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/serialized-form.html b/doc/serialized-form.html index 98c2f06..eb5ad03 100644 --- a/doc/serialized-form.html +++ b/doc/serialized-form.html @@ -2,9 +2,9 @@ - + Serialized Form (Halo 3 Reclaimer) - + @@ -166,6 +166,6 @@ -

    Copyright 2025. Documentation licensed under GFDL v1.3.

    +

    Copyright 2025, 2026. Documentation licensed under GFDL v1.3.

    diff --git a/doc/user-manual-editable.odt b/doc/user-manual-editable.odt index 9bcb026..f0c1ad9 100644 Binary files a/doc/user-manual-editable.odt and b/doc/user-manual-editable.odt differ diff --git a/doc/user-manual.pdf b/doc/user-manual.pdf index 4a98f7c..81f8e4c 100644 Binary files a/doc/user-manual.pdf and b/doc/user-manual.pdf differ diff --git a/halo3-reclaimer.jar b/halo3-reclaimer.jar index 990df86..12ddbc3 100644 Binary files a/halo3-reclaimer.jar and b/halo3-reclaimer.jar differ diff --git a/src/Arbiter.java b/src/Arbiter.java index b260dc4..8f3ed6e 100644 --- a/src/Arbiter.java +++ b/src/Arbiter.java @@ -8,10 +8,13 @@ import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.Map; /** * Arbiter class that determines file type and routes to appropriate extractor. @@ -21,24 +24,31 @@ import java.util.List; * Cortana first, which copies the Halo 3 files out of the Xbox file system; * other files on the stick are ignored then.

    * -*

    Halo 3 keeps every screenshot and every Forge map in its own package, -* so the Arbiter looks inside each file: a package holding +*

    Halo 3 keeps every screenshot, Forge map and custom game type in its own +* package, so the Arbiter looks inside each file: a package holding * screen.shot is a screenshot, one holding sandbox.map -* is a map. Screenshots are checked first, then maps. Anything else is skipped. -* Only when the content says nothing (DDS, TGA, raw textures) the file name -* decides, like in version 1.0.

    +* is a map and one holding variant.slayer, variant.ctf... +* is a game mode. Screenshots are checked first, then maps, then game modes. +* Anything else is skipped. Only when the content says nothing (DDS, TGA, +* raw textures) the file name decides, like in version 1.0.

    * -*

    Results go to two folders in the output directory: -* screenshots-reclaimed and maps-reclaimed.

    +*

    Results go to halo-3-reclaimed-files in the output directory, +* sorted into screenshots, maps and game-modes. +* Files are named after their in-game names, see {@link #saveFiles}.

    * -* @version 1.1 +* @version 1.2 */ public class Arbiter { - // folders created inside the output directory - public static final String SCREENSHOTS_DIR = "screenshots-reclaimed"; - public static final String MAPS_DIR = "maps-reclaimed"; + // everything goes into one folder inside the output directory, sorted by kind + public static final String OUTPUT_DIR = "halo-3-reclaimed-files"; + public static final String SCREENSHOTS_DIR = "screenshots"; + public static final String MAPS_DIR = "maps"; + public static final String GAME_MODES_DIR = "game-modes"; + + // output folders of older test builds, never read back either + private static final String[] OLD_OUTPUT_DIRS = { "screenshots-reclaimed", "maps-reclaimed" }; // the file inside a package that holds a screenshot private static final String SCREENSHOT_FILE = "screen.shot"; @@ -49,12 +59,12 @@ public class Arbiter { // how deep to look into sub folders private static final int MAX_FOLDER_DEPTH = 8; - private enum Kind { SCREENSHOT, MAP, OTHER } + private enum Kind { SCREENSHOT, MAP, GAME_MODE, OTHER } /** - * Reclaims all screenshots and maps found in a file or folder. + * Reclaims all screenshots, maps and game modes found in a file or folder. * - * @return what was reclaimed, for example "Reclaimed 2 pictures and 1 map" + * @return what was reclaimed, for example "Reclaimed 2 pictures, 1 map and 3 game modes" * @throws Exception if nothing could be reclaimed */ public static String processFile(File input, File outputDir) throws Exception { @@ -78,13 +88,15 @@ public class Arbiter { } /** - * Sorts the files out and runs the right reclaimer on each, screenshots first. + * Sorts the files out and runs the right reclaimer on each: screenshots first, + * then maps, then game modes. * folder: there are many files to go through, root: where console paths start. */ private static String reclaim(List files, File outputDir, boolean folder, File root) throws Exception { - // sort everything out first, screenshots go first + // sort everything out first List screenshots = new ArrayList(); List maps = new ArrayList(); + List gameModes = new ArrayList(); String skipReason = null; for (File file : files) { StringBuilder reason = new StringBuilder(); @@ -94,6 +106,8 @@ public class Arbiter { screenshots.add(file); } else if (kind == Kind.MAP) { maps.add(file); + } else if (kind == Kind.GAME_MODE) { + gameModes.add(file); } else { // short reason for the status bar, the detail only goes to the console skipReason = reason.toString(); @@ -106,23 +120,24 @@ public class Arbiter { } } - if (screenshots.isEmpty() && maps.isEmpty()) { + if (screenshots.isEmpty() && maps.isEmpty() && gameModes.isEmpty()) { if (folder) { throw ErrorHandler.makeException(ErrorHandler.UNKNOWN_FILE + ": no Halo 3 files found"); } throw ErrorHandler.makeException(skipReason); } if (folder) { - System.out.println("Found " + count(screenshots.size(), "screenshot") - + " and " + count(maps.size(), "map")); + System.out.println("Found " + list(count(screenshots.size(), "screenshot"), + count(maps.size(), "map"), count(gameModes.size(), "game mode"))); } + File reclaimedDir = new File(outputDir, OUTPUT_DIR); int failed = 0; Exception lastError = null; int pictures = 0; if (!screenshots.isEmpty()) { - File screenshotsDir = outputFolder(outputDir, SCREENSHOTS_DIR); + File screenshotsDir = outputFolder(reclaimedDir, SCREENSHOTS_DIR); for (File file : screenshots) { System.out.println("Detected as screenshot: " + shown(root, file)); try { @@ -138,7 +153,7 @@ public class Arbiter { int reclaimedMaps = 0; if (!maps.isEmpty()) { - File mapsDir = outputFolder(outputDir, MAPS_DIR); + File mapsDir = outputFolder(reclaimedDir, MAPS_DIR); for (File file : maps) { System.out.println("Detected as Forge map: " + shown(root, file)); try { @@ -152,17 +167,33 @@ public class Arbiter { } } - if (pictures + reclaimedMaps == 0) { + int reclaimedGameModes = 0; + if (!gameModes.isEmpty()) { + File gameModesDir = outputFolder(reclaimedDir, GAME_MODES_DIR); + for (File file : gameModes) { + System.out.println("Detected as game mode: " + shown(root, file)); + try { + new GameModeReclaimer().extractGameMode(file, gameModesDir); + reclaimedGameModes++; + } catch (Exception e) { + failed++; + lastError = e; + System.out.println("Could not reclaim " + shown(root, file) + ": " + e.getMessage()); + } + } + } + + if (pictures + reclaimedMaps + reclaimedGameModes == 0) { if (failed == 1) { throw lastError; } throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": all " + failed + " files failed"); } - List reclaimed = new ArrayList(); - if (pictures > 0) reclaimed.add(count(pictures, "picture")); - if (reclaimedMaps > 0) reclaimed.add(count(reclaimedMaps, "map")); - String result = "Reclaimed " + String.join(" and ", reclaimed); + String result = "Reclaimed " + list( + pictures > 0 ? count(pictures, "picture") : null, + reclaimedMaps > 0 ? count(reclaimedMaps, "map") : null, + reclaimedGameModes > 0 ? count(reclaimedGameModes, "game mode") : null); if (failed > 0) { result += " (" + failed + " failed)"; } @@ -170,14 +201,15 @@ public class Arbiter { } /** - * Tells what a file is by looking inside it. Screenshots are checked first. - * When the file is neither, a short error goes into reason and, when - * there is more to say, an explanation for the console into detail. + * Tells what a file is by looking inside it. Screenshots are checked first, + * then maps, then game modes. When the file is none of them, a short error + * goes into reason and, when there is more to say, an explanation for the + * console into detail. */ private static Kind detect(File file, StringBuilder reason, StringBuilder detail) { if (file.length() > MAX_FILE_SIZE) { reason.append(ErrorHandler.CANT_PROCESS + ": file too big"); - detail.append(file.length() + " bytes, too big for a screenshot or map"); + detail.append(file.length() + " bytes, too big for a Halo 3 save"); return Kind.OTHER; } @@ -194,8 +226,9 @@ public class Arbiter { List names = MapReclaimer.listPackageFiles(data); if (containsName(names, SCREENSHOT_FILE)) return Kind.SCREENSHOT; if (containsName(names, MapReclaimer.MAP_VARIANT_FILE)) return Kind.MAP; - // game types, films... - reason.append(ErrorHandler.CANT_PROCESS + ": not a screenshot or map"); + if (GameModeReclaimer.findGameModeFile(names) != null) return Kind.GAME_MODE; + // films, campaign saves... + reason.append(ErrorHandler.CANT_PROCESS + ": not a screenshot, map or game mode"); detail.append(names.isEmpty() ? "the package is empty" : "the package holds " + String.join(", ", names)); return Kind.OTHER; @@ -204,13 +237,14 @@ public class Arbiter { reason.append(e.getMessage() != null ? e.getMessage() : ErrorHandler.CANT_PROCESS); } } else if (MapReclaimer.isBLFFile(data)) { - // screen.shot or sandbox.map already taken out of its package + // screen.shot, sandbox.map or variant.* already taken out of its package int fileType = MapReclaimer.readBlfFileType(data); if (fileType == MapReclaimer.FILE_TYPE_SCREENSHOT) return Kind.SCREENSHOT; if (fileType == MapReclaimer.FILE_TYPE_MAP_VARIANT) return Kind.MAP; + if (GameModeReclaimer.isGameModeType(fileType)) return Kind.GAME_MODE; if (fileType >= 0) { - reason.append(ErrorHandler.CANT_PROCESS + ": not a screenshot or map"); - detail.append("Halo file of type " + fileType + " (game type, film...)"); + reason.append(ErrorHandler.CANT_PROCESS + ": not a screenshot, map or game mode"); + detail.append("Halo file of type " + fileType + " (film, campaign save...)"); return Kind.OTHER; } } @@ -241,6 +275,85 @@ public class Arbiter { return false; } + /** + * Saves reclaimed files under their in-game name: all lowercase, spaces as + * underscores, e.g. guardian_infection.map and + * guardian_infection.package. When a different file already uses + * that name, a number is added (guardian_infection_2.map); the very + * same file reclaimed again just keeps its name, so running the reclaimer twice + * doesn't make copies. + * + * @param inGameName name stored in the file, can be empty + * @param fallback used when the in-game name is empty, e.g. the package name + * @param files extension (with the dot) and contents of each file to save + * @return the name used, without extension + * @throws Exception if a file can't be written + */ + static String saveFiles(File folder, String inGameName, String fallback, Map files) throws Exception { + String base = toFileName(inGameName); + if (base.isEmpty()) base = toFileName(fallback); + if (base.isEmpty()) base = "unnamed"; + + for (int number = 1; number < 10000; number++) { + String name = (number == 1) ? base : base + "_" + number; + if (isFree(folder, name, files)) { + for (Map.Entry file : files.entrySet()) { + File target = new File(folder, name + file.getKey()); + writeFile(target, file.getValue()); + System.out.println("Saved as: " + target.getPath()); + } + return name; + } + } + System.out.println("Too many files named " + base + " in " + folder.getPath()); + throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": too many same names"); + } + + /** + * In-game name to file name: lowercase, spaces become underscores, and so does + * everything Windows doesn't allow in file names. + */ + static String toFileName(String name) { + if (name == null) return ""; + String lower = name.trim().toLowerCase(Locale.ROOT); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < lower.length(); i++) { + char c = lower.charAt(i); + boolean forbidden = c == ' ' || Character.isISOControl(c) || "\\/:*?\"<>|".indexOf(c) >= 0; + sb.append(forbidden ? '_' : c); + } + String fileName = sb.toString(); + // Windows also refuses names ending in a dot and a few device names like "con" + if (fileName.endsWith(".")) fileName += "_"; + if (fileName.matches("con|prn|aux|nul|com[1-9]|lpt[1-9]")) fileName += "_"; + return fileName; + } + + // true when each of these files is new, or already there with exactly the same contents + private static boolean isFree(File folder, String name, Map files) { + for (Map.Entry file : files.entrySet()) { + File existing = new File(folder, name + file.getKey()); + if (!existing.exists()) continue; + try { + if (!existing.isFile() || existing.length() != file.getValue().length + || !Arrays.equals(readFile(existing), file.getValue())) { + return false; + } + } catch (IOException e) { + return false; + } + } + return true; + } + + private static void writeFile(File file, byte[] data) throws Exception { + try (FileOutputStream out = new FileOutputStream(file)) { + out.write(data); + } catch (IOException e) { + throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": " + e.getMessage()); + } + } + private static List listFiles(File folder) { List files = new ArrayList(); collectFiles(folder, files, 0); @@ -267,8 +380,7 @@ public class Arbiter { Arrays.sort(children); for (File child : children) { if (child.isDirectory()) { - String name = child.getName(); - if (!name.equals(SCREENSHOTS_DIR) && !name.equals(MAPS_DIR)) { + if (!isOutputFolder(child.getName())) { collectFiles(child, files, depth + 1); } } else if (child.isFile()) { @@ -277,6 +389,14 @@ public class Arbiter { } } + private static boolean isOutputFolder(String name) { + if (name.equals(OUTPUT_DIR)) return true; + for (String old : OLD_OUTPUT_DIRS) { + if (name.equals(old)) return true; + } + return false; + } + /** * Returns the output sub folder, creates it when needed. */ @@ -300,4 +420,14 @@ public class Arbiter { private static String count(int number, String word) { return number + " " + word + (number == 1 ? "" : "s"); } + + // "a", "a and b", "a, b and c"; empty parts are left out + private static String list(String... parts) { + List used = new ArrayList(); + for (String part : parts) { + if (part != null) used.add(part); + } + if (used.size() <= 1) return used.isEmpty() ? "" : used.get(0); + return String.join(", ", used.subList(0, used.size() - 1)) + " and " + used.get(used.size() - 1); + } } diff --git a/src/ErrorHandler.java b/src/ErrorHandler.java index 37789c7..64cdd17 100644 --- a/src/ErrorHandler.java +++ b/src/ErrorHandler.java @@ -10,15 +10,15 @@ * *

    This class just stores error messages and prints them when needed.

    * -* @version 1.0 +* @version 1.1 */ public class ErrorHandler { // Error messages public static final String UNKNOWN_FILE = "343: Not a Halo game (file)"; public static final String TOO_MANY_ARGS = "002: Too many arguments"; - public static final String BAD_INPUT = "123: Input file problem"; - public static final String BAD_OUTPUT = "123: Output directory problem"; + public static final String BAD_INPUT = "011: Input file problem"; + public static final String BAD_OUTPUT = "012: Output directory problem"; public static final String CANT_PROCESS = "003: Cannot process file"; public static final String NOT_READY = "117: Feature not ready yet"; public static final String NEED_SELECTION = "001: Please select file and directory"; @@ -46,4 +46,4 @@ public class ErrorHandler { public static Exception makeException(String errorMessage) { return new Exception(errorMessage); } -} \ No newline at end of file +} diff --git a/src/GameModeReclaimer.java b/src/GameModeReclaimer.java new file mode 100644 index 0000000..34276ed --- /dev/null +++ b/src/GameModeReclaimer.java @@ -0,0 +1,175 @@ +/* +* Halo 3 Reclaimer [GameModeReclaimer.java] +* player data extractor from Xbox 360 version of Halo 3 +* copyright 2026, David Polakovic, licensed under GPLv3 +* contact: email@dpolakovic.space +*/ + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** +* Core extraction engine for custom game types (game modes). +* +*

    Halo 3 saves every custom game type as its own Xbox 360 package, just +* like the Forge maps. The package holds one file named after the game's +* rules, for example variant.slayer or variant.ctf. +* That file is the game mode itself, all of its settings, stored in Bungie's +* BLF format. The package and BLF readers are shared with MapReclaimer.

    +* +*

    Supported input formats:

    +*
      +*
    • STFS container files holding a Halo 3 game type
    • +*
    • already extracted variant.* files (BLF)
    • +*
    +* +*

    Two files are saved, named after the game mode (see Arbiter.saveFiles):

    +*
      +*
    • team_snipers.slayer: byte for byte the file the console +* keeps inside the package, its ending tells the game type
    • +*
    • team_snipers.package: the untouched package, which tools +* like Horizon or Velocity can sign for another player's profile +* (only when the input was a package)
    • +*
    +* +* @version 1.0 +*/ + +public class GameModeReclaimer { + + // the file inside the package is called variant. + private static final String GAME_MODE_FILE_PREFIX = "variant"; + + // saved game file types 1 to 9 are the game modes, in this order + private static final int FIRST_GAME_MODE_TYPE = 1; + private static final int LAST_GAME_MODE_TYPE = 9; + private static final String[] GAME_MODE_NAMES = { + null, "Capture the Flag", "Slayer", "Oddball", "King of the Hill", + "Juggernaut", "Territories", "Assault", "Infection", "VIP" + }; + + // file endings for bare files whose name doesn't tell (packages always do) + private static final String[] GAME_MODE_EXTENSIONS = { + null, "ctf", "slayer", "oddball", "king", "juggernaut", + "territories", "assault", "infection", "vip" + }; + + /** + * Extracts the game mode from an Xbox 360 file and saves it, together with + * the package it came in, under its in-game name in the output directory. + * + * @throws Exception if the file cannot be read, is not a Halo 3 game mode, + * or is damaged + */ + public void extractGameMode(File inputFile, File outputDir) throws Exception { + byte[] fileData = MapReclaimer.readInputFile(inputFile); + + System.out.println("File size: " + fileData.length + " bytes"); + + byte[] variantData; + byte[] packageData = null; + String extension; + MapReclaimer.ContentInfo info; + try { + if (MapReclaimer.isSTFSFile(fileData)) { + System.out.println("Detected STFS file format"); + String variantFile = findGameModeFile(MapReclaimer.listPackageFiles(fileData)); + variantData = (variantFile == null) ? null : MapReclaimer.readPackageFile(fileData, variantFile); + if (variantData == null) { + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": no game mode inside"); + } + extension = extensionOf(variantFile); + packageData = fileData; + } else if (MapReclaimer.isBLFFile(fileData)) { + System.out.println("Detected extracted game mode (BLF) file"); + variantData = fileData; + extension = extensionOf(inputFile.getName()); + } else { + System.out.println("Not an Xbox 360 package or game mode"); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a game mode"); + } + info = readGameModeInfo(variantData); + } catch (RuntimeException e) { + // a bug or a file layout nobody has seen yet, keep the GUI informed + e.printStackTrace(); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged or unsupported file"); + } + + if (extension == null) { + extension = GAME_MODE_EXTENSIONS[info.fileType]; + } + + System.out.println("Game mode: \"" + info.name + "\" (" + GAME_MODE_NAMES[info.fileType] + ")"); + if (!info.description.isEmpty()) { + System.out.println("Description: " + info.description); + } + if (!info.author.isEmpty()) { + System.out.println("Author: " + info.author); + } + System.out.println("Game mode size: " + variantData.length + " bytes"); + + // the game mode itself, and the package it came in, ready to be signed for another player + Map files = new LinkedHashMap(); + files.put("." + extension, variantData); + if (packageData != null) { + files.put(MapReclaimer.PACKAGE_EXTENSION, packageData); + } else { + System.out.println("No package to save, the input was a bare game mode file"); + } + Arbiter.saveFiles(outputDir, info.name, MapReclaimer.baseName(inputFile), files); + } + + /** + * The game mode file among the files of a package (variant.slayer, + * variant.ctf...), or null if there is none. + */ + static String findGameModeFile(List names) { + for (String name : names) { + if (name.toLowerCase(Locale.ROOT).startsWith(GAME_MODE_FILE_PREFIX)) return name; + } + return null; + } + + /** + * True for the saved game file types of game modes (1 to 9). + */ + static boolean isGameModeType(int fileType) { + return fileType >= FIRST_GAME_MODE_TYPE && fileType <= LAST_GAME_MODE_TYPE; + } + + /** + * Makes sure the BLF file really is a complete game mode and reads its metadata. + */ + private MapReclaimer.ContentInfo readGameModeInfo(byte[] blf) throws Exception { + if (!MapReclaimer.isBLFFile(blf)) { + System.out.println("The game mode file is not a BLF file"); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a game mode"); + } + + MapReclaimer.ContentInfo info = MapReclaimer.readContentInfo(blf); + if (info == null) { + System.out.println("Game mode has a broken chunk"); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged game mode"); + } + if (!info.complete) { + System.out.println("Game mode is incomplete, its end of file chunk is missing"); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged game mode"); + } + if (!isGameModeType(info.fileType)) { + System.out.println("This Halo file is of type " + info.fileType + ", not a game mode"); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a game mode"); + } + return info; + } + + // "variant.slayer" -> "slayer", null when the name doesn't tell + private static String extensionOf(String fileName) { + String lower = fileName.toLowerCase(Locale.ROOT); + if (!lower.startsWith(GAME_MODE_FILE_PREFIX + ".")) return null; + String extension = Arbiter.toFileName(lower.substring(GAME_MODE_FILE_PREFIX.length() + 1)); + return extension.isEmpty() ? null : extension; + } +} diff --git a/src/GraphicalUI.java b/src/GraphicalUI.java index 08112f6..3c39f79 100644 --- a/src/GraphicalUI.java +++ b/src/GraphicalUI.java @@ -17,7 +17,7 @@ import java.io.File; *

    This class contains all the GUI components and * logic for the application's windowed interface

    * -* @version 1.1 +* @version 1.2 */ public class GraphicalUI extends JFrame { @@ -161,7 +161,7 @@ public class GraphicalUI extends JFrame { */ private void showVersionInfo() { String versionText = "Halo 3 Reclaimer\n\n" + - "Version: 0.2\n\n" + + "Version: 0.3\n\n" + "player data extractor from Xbox 360 version of Halo 3\n" + "copyright 2025, 2026, David Polakovic, licensed under GPLv3\n"; @@ -182,7 +182,8 @@ public class GraphicalUI extends JFrame { "The tool will extract your user data from the\n" + "Xbox 360 save. Currently supports:\n" + "-screenshots\n" + - "-Forge maps\n\n"; + "-Forge map variants\n" + + "-Custom game types\n\n"; JOptionPane.showMessageDialog(this, helpText, "Help", JOptionPane.INFORMATION_MESSAGE); diff --git a/src/Main.java b/src/Main.java index 58f68f3..29db951 100644 --- a/src/Main.java +++ b/src/Main.java @@ -50,4 +50,4 @@ public class Main { GraphicalUI.main(); } } -} \ No newline at end of file +} diff --git a/src/MapReclaimer.java b/src/MapReclaimer.java index 9a136c6..7823c2e 100644 --- a/src/MapReclaimer.java +++ b/src/MapReclaimer.java @@ -8,13 +8,14 @@ import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Core extraction engine for Forge map variants. @@ -29,7 +30,8 @@ import java.util.List; *

    Unlike screenshots, the map has no magic bytes worth scanning for, * so this class reads the STFS file system properly: it walks the file * table, follows the block chain of sandbox.map and checks -* each block against the package hash tables.

    +* each block against the package hash tables. These package and BLF readers +* are shared with the other reclaimers.

    * *

    Supported input formats:

    *
      @@ -37,10 +39,16 @@ import java.util.List; *
    • already extracted sandbox.map files (BLF)
    • *
    * -*

    The saved file is not converted in any way. It is byte for byte the -* sandbox.map the console keeps inside the package.

    +*

    Two files are saved, named after the map (see Arbiter.saveFiles):

    +*
      +*
    • guardian_infection.map: byte for byte the +* sandbox.map the console keeps inside the package
    • +*
    • guardian_infection.package: the untouched package, which +* tools like Horizon or Velocity can sign for another player's profile +* (only when the input was a package)
    • +*
    * -* @version 1.0 +* @version 1.1 */ public class MapReclaimer { @@ -64,6 +72,9 @@ public class MapReclaimer { // the file inside the package that holds the Forge map static final String MAP_VARIANT_FILE = "sandbox.map"; + // packages are saved untouched under this extension + static final String PACKAGE_EXTENSION = ".package"; + // BLF format constants (the format of sandbox.map) private static final int BLF_START = 0x5F626C66; // "_blf" private static final int BLF_END = 0x5F656F66; // "_eof" @@ -92,23 +103,29 @@ public class MapReclaimer { }; /** - * Extracts the Forge map from an Xbox 360 file and saves it as - * <input name>-reclaimed.map in the output directory. + * Extracts the Forge map from an Xbox 360 file and saves it, together with + * the package it came in, under the map's in-game name in the output directory. * * @throws Exception if the file cannot be read, is not a Halo 3 Forge map, * or is damaged */ public void extractMap(File inputFile, File outputDir) throws Exception { - byte[] fileData = readFile(inputFile); + byte[] fileData = readInputFile(inputFile); System.out.println("File size: " + fileData.length + " bytes"); byte[] mapData; - MapInfo info; + byte[] packageData = null; + ContentInfo info; try { if (isSTFSFile(fileData)) { System.out.println("Detected STFS file format"); - mapData = extractFromSTFS(fileData); + mapData = readPackageFile(fileData, MAP_VARIANT_FILE); + if (mapData == null) { + // the "Package contains" lines tell what is in there instead + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": no Forge map inside"); + } + packageData = fileData; } else if (isBLFFile(fileData)) { System.out.println("Detected extracted map variant (BLF) file"); mapData = fileData; @@ -135,26 +152,25 @@ public class MapReclaimer { } System.out.println("Map variant size: " + mapData.length + " bytes"); - // generate output filename, same scheme as the screenshots - String baseName = inputFile.getName(); - int dotIndex = baseName.lastIndexOf('.'); - if (dotIndex > 0) { - baseName = baseName.substring(0, dotIndex); + // the map itself, and the package it came in, ready to be signed for another player + Map files = new LinkedHashMap(); + files.put(".map", mapData); + if (packageData != null) { + files.put(PACKAGE_EXTENSION, packageData); + } else { + System.out.println("No package to save, the input was a bare map variant"); } - String outputFileName = baseName + "-reclaimed.map"; - File outputFile = new File(outputDir, outputFileName); - - writeFile(outputFile, mapData); - System.out.println("Saved as: " + outputFile.getAbsolutePath()); + Arbiter.saveFiles(outputDir, info.name, baseName(inputFile), files); } /** - * Reads the entire contents of a file into a byte array. + * Reads the entire contents of an input file into a byte array. + * Halo 3 packages are small, anything huge is refused. */ - private byte[] readFile(File file) throws Exception { + static byte[] readInputFile(File file) throws Exception { long length = file.length(); if (length > MAX_INPUT_SIZE) { - throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": file too big for a Forge map"); + throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": file too big"); } byte[] data = new byte[(int) length]; @@ -166,12 +182,13 @@ public class MapReclaimer { return data; } - private void writeFile(File file, byte[] data) throws Exception { - try (FileOutputStream out = new FileOutputStream(file)) { - out.write(data); - } catch (IOException e) { - throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": " + e.getMessage()); - } + /** + * The file name without its extension, the fallback when there is no in-game name. + */ + static String baseName(File file) { + String name = file.getName(); + int dotIndex = name.lastIndexOf('.'); + return (dotIndex > 0) ? name.substring(0, dotIndex) : name; } static boolean isSTFSFile(byte[] data) { @@ -186,7 +203,7 @@ public class MapReclaimer { /** * Lists the files inside a Halo 3 package. The Arbiter uses this to tell - * screenshots (screen.shot) from maps (sandbox.map) without trusting file names. + * screenshots, maps and game modes apart without trusting file names. * * @throws Exception if the package is damaged or belongs to another game */ @@ -206,23 +223,12 @@ public class MapReclaimer { } /** - * Saved game file type of an already extracted BLF file - * (FILE_TYPE_SCREENSHOT, FILE_TYPE_MAP_VARIANT...), or -1 if it can't be told. - */ - static int readBlfFileType(byte[] blf) { - if (!isBLFFile(blf)) return -1; - int[] chunks = findBlfChunks(blf); - if (chunks == null) return -1; - if (hasMetadata(blf, chunks[0])) { - return readInt32(blf, chunks[0] + METADATA_OFFSET + METADATA_FILE_TYPE); - } - return chunks[1] >= 0 ? FILE_TYPE_MAP_VARIANT : -1; - } - - /** - * Finds sandbox.map in the package and returns its contents. + * Reads one file out of a Halo 3 package and prints what the package holds. + * + * @return the file contents, or null when the package has no file of that name + * @throws Exception if the package is damaged or belongs to another game */ - private byte[] extractFromSTFS(byte[] data) throws Exception { + static byte[] readPackageFile(byte[] data, String fileName) throws Exception { StfsPackage stfs = new StfsPackage(data); System.out.printf("Package: \"%s\", title ID %08X%n", stfs.displayName, stfs.titleId); @@ -230,76 +236,106 @@ public class MapReclaimer { throw ErrorHandler.makeException(ErrorHandler.UNKNOWN_FILE + ": not a Halo 3 package"); } - StfsEntry mapEntry = null; + StfsEntry wanted = null; for (StfsEntry entry : stfs.entries) { System.out.println("Package contains: " + entry.name + (entry.isDirectory ? " (folder)" : " (" + entry.size + " bytes)")); - if (mapEntry == null && !entry.isDirectory && entry.name.equalsIgnoreCase(MAP_VARIANT_FILE)) { - mapEntry = entry; + if (wanted == null && !entry.isDirectory && entry.name.equalsIgnoreCase(fileName)) { + wanted = entry; } } + if (wanted == null) return null; - if (mapEntry == null) { - // the "Package contains" lines above tell what is in there instead - throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": no Forge map inside"); - } - - byte[] mapData = stfs.readFile(mapEntry); + byte[] content = stfs.readFile(wanted); if (stfs.badBlocks > 0) { - System.out.println("Warning: " + stfs.badBlocks + " block(s) of " + MAP_VARIANT_FILE + System.out.println("Warning: " + stfs.badBlocks + " block(s) of " + wanted.name + " do not match the package hashes, the package may be damaged"); } else { System.out.println("Package hashes OK"); } - return mapData; + return content; + } + + /** + * Saved game file type of an already extracted BLF file + * (FILE_TYPE_SCREENSHOT, FILE_TYPE_MAP_VARIANT...), or -1 if it can't be told. + */ + static int readBlfFileType(byte[] blf) { + ContentInfo info = readContentInfo(blf); + if (info == null) return -1; + if (info.fileType >= 0) return info.fileType; + return info.hasMapVariant ? FILE_TYPE_MAP_VARIANT : -1; + } + + /** + * What a saved Halo 3 file (map, game mode, screenshot) says about itself. + */ + static final class ContentInfo { + String name = ""; + String description = ""; + String author = ""; + int fileType = -1; + int mapId = -1; + boolean complete; // it ends with its _eof chunk + boolean hasMapVariant; // it has an mvar chunk } /** - * Walks the BLF chunks of a map variant, makes sure it really is a Forge map - * and reads its name, description and author. + * Reads the metadata of a BLF file: name, description, author, file type... + * + * @return the metadata, or null if it is not a BLF file or a chunk is broken */ - private MapInfo readMapInfo(byte[] blf) throws Exception { + static ContentInfo readContentInfo(byte[] blf) { + if (!isBLFFile(blf)) return null; + int[] chunks = findBlfChunks(blf); + if (chunks == null) return null; + + ContentInfo info = new ContentInfo(); + info.hasMapVariant = chunks[1] >= 0; + info.complete = chunks[2] >= 0; + + // both chdr and mvar start with the same metadata block, chdr is the official one + int metadata = -1; + if (hasMetadata(blf, chunks[0])) { + metadata = chunks[0] + METADATA_OFFSET; + } else if (hasMetadata(blf, chunks[1])) { + metadata = chunks[1] + METADATA_OFFSET; + } + if (metadata >= 0) { + info.name = readUtf16(blf, metadata + METADATA_NAME, 32); + info.description = readUtf8(blf, metadata + METADATA_DESCRIPTION, 128); + info.author = readUtf8(blf, metadata + METADATA_AUTHOR, 16); + info.fileType = readInt32(blf, metadata + METADATA_FILE_TYPE); + info.mapId = readInt32(blf, metadata + METADATA_MAP_ID); + } + return info; + } + + /** + * Makes sure the BLF file really is a complete Forge map and reads its metadata. + */ + private ContentInfo readMapInfo(byte[] blf) throws Exception { if (!isBLFFile(blf)) { System.out.println(MAP_VARIANT_FILE + " is not a map variant (BLF) file"); throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a Forge map"); } - int[] chunks = findBlfChunks(blf); - if (chunks == null) { + ContentInfo info = readContentInfo(blf); + if (info == null) { System.out.println("Map variant has a broken chunk"); throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged map variant"); } - int contentHeader = chunks[0]; - int mapVariant = chunks[1]; - if (chunks[2] < 0) { + if (!info.complete) { System.out.println("Map variant is incomplete, its end of file chunk is missing"); throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged map variant"); } - - // both chdr and mvar start with the same metadata block, chdr is the official one - int metadata = -1; - if (hasMetadata(blf, contentHeader)) { - metadata = contentHeader + METADATA_OFFSET; - } else if (hasMetadata(blf, mapVariant)) { - metadata = mapVariant + METADATA_OFFSET; - } - - int fileType = metadata >= 0 ? readInt32(blf, metadata + METADATA_FILE_TYPE) : -1; - if (mapVariant < 0 && fileType != FILE_TYPE_MAP_VARIANT) { - if (fileType >= 0 && fileType < FILE_TYPE_NAMES.length) { - System.out.println("This file is a " + FILE_TYPE_NAMES[fileType]); + if (!info.hasMapVariant && info.fileType != FILE_TYPE_MAP_VARIANT) { + if (info.fileType >= 0 && info.fileType < FILE_TYPE_NAMES.length) { + System.out.println("This file is a " + FILE_TYPE_NAMES[info.fileType]); } throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a Forge map"); } - - MapInfo info = new MapInfo(); - if (metadata >= 0) { - info.name = readUtf16(blf, metadata + METADATA_NAME, 32); - info.description = readUtf8(blf, metadata + METADATA_DESCRIPTION, 128); - info.author = readUtf8(blf, metadata + METADATA_AUTHOR, 16); - info.mapId = readInt32(blf, metadata + METADATA_MAP_ID); - } return info; } @@ -343,16 +379,6 @@ public class MapReclaimer { return chunkSize >= METADATA_OFFSET + METADATA_MAP_ID + 4; } - /** - * What the map variant says about itself. - */ - private static final class MapInfo { - String name = ""; - String description = ""; - String author = ""; - int mapId = -1; - } - /** * One entry of the STFS file table. */ diff --git a/src/ScreenshotReclaimer.java b/src/ScreenshotReclaimer.java index 18b8428..abdf6a4 100644 --- a/src/ScreenshotReclaimer.java +++ b/src/ScreenshotReclaimer.java @@ -1,7 +1,7 @@ /* * Halo 3 Reclaimer [ScreenshotReclaimer.java] * player data extractor from Xbox 360 version of Halo 3 -* copyright 2025, David Polakovic, licensed under GPLv3 +* copyright 2025, 2026, David Polakovic, licensed under GPLv3 * contact: email@dpolakovic.space */ @@ -10,6 +10,8 @@ import java.awt.image.BufferedImage; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.LinkedHashMap; +import java.util.Map; /** * Core extraction engine for screenshots. @@ -33,7 +35,7 @@ import java.nio.ByteOrder; *
  • Standard RGBA and RGB formats
  • * * -* @version 1.0 +* @version 1.1 */ public class ScreenshotReclaimer { @@ -56,7 +58,10 @@ public class ScreenshotReclaimer { private static final int STFS_MAGIC_CON = 0x434F4E20; // "CON " private static final int STFS_MAGIC_LIVE = 0x4C495645; // "LIVE" private static final int STFS_MAGIC_PIRS = 0x50495253; // "PIRS" - + + // the file inside a package that holds the screenshot and its name + private static final String SCREENSHOT_FILE = "screen.shot"; + /** * Extracts a screenshot from an Xbox 360 file and saves it as a JPEG. * @@ -101,19 +106,32 @@ public class ScreenshotReclaimer { } System.out.println("Successfully extracted image: " + image.getWidth() + "x" + image.getHeight()); - - // generate output filename - String baseName = inputFile.getName(); - int dotIndex = baseName.lastIndexOf('.'); - if (dotIndex > 0) { - baseName = baseName.substring(0, dotIndex); - } - String outputFileName = baseName + "-reclaimed.jpg"; - File outputFile = new File(outputDir, outputFileName); - if (!ImageIO.write(image, "jpg", outputFile)) { + // plain JPEG, named after the screenshot's in-game name like the maps and game modes + ByteArrayOutputStream jpeg = new ByteArrayOutputStream(); + if (!ImageIO.write(image, "jpg", jpeg)) { throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS); } + Map files = new LinkedHashMap(); + files.put(".jpg", jpeg.toByteArray()); + Arbiter.saveFiles(outputDir, readScreenshotName(fileData), MapReclaimer.baseName(inputFile), files); + } + + /** + * The in-game name of the screenshot, read from its package, or "" if unknown. + */ + private String readScreenshotName(byte[] data) { + try { + byte[] shot = isSTFSFile(data) ? MapReclaimer.readPackageFile(data, SCREENSHOT_FILE) : data; + MapReclaimer.ContentInfo info = (shot == null) ? null : MapReclaimer.readContentInfo(shot); + if (info != null && !info.name.isEmpty()) { + System.out.println("Screenshot: \"" + info.name + "\""); + return info.name; + } + } catch (Exception e) { + // the picture is what counts, the name is a bonus + } + return ""; } /** @@ -706,4 +724,4 @@ public class ScreenshotReclaimer { return hasNonBlack && hasNonWhite; } -} \ No newline at end of file +} diff --git a/src/TerminalUI.java b/src/TerminalUI.java index 015adad..8e909a0 100644 --- a/src/TerminalUI.java +++ b/src/TerminalUI.java @@ -12,15 +12,15 @@ import java.io.File; * *

    This class contains all the command-line interface logic.

    * -* @version 1.1 +* @version 1.2 */ public class TerminalUI { public static void showVersion() { System.out.println("Halo 3 Reclaimer"); - System.out.println("Version: 1.0"); + System.out.println("Version: 0.3"); System.out.println("player data extractor from Xbox 360 version of Halo 3"); - System.out.println("copyright 2025, David Polakovic, licensed under GPLv3"); + System.out.println("copyright 2025, 2026, David Polakovic, licensed under GPLv3"); } @@ -91,7 +91,8 @@ public class TerminalUI { // use Arbiter to process the file or folder String result = Arbiter.processFile(inputFile, outputDir); - System.out.println(result + " into: " + outputDir.getAbsolutePath()); + System.out.println(result + " into: " + + new File(outputDir, Arbiter.OUTPUT_DIR).getAbsolutePath()); } catch (Exception e) { // parse error code from exception message if it starts with a number