Compare commits

...
2 Commits
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 7da699b035 Merge feature/disc-languages: the European disc in Spanish and Italian imported, in the headset's language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:31 +02:00
Stefan MaierandClaude Opus 5.5 c5c604bb95 Import the European disc in Spanish and Italian, in the headset's language
That disc has no maps folder but one for each language, maps_es and
maps_it, and the disc image setup stopped: "The disc image has no maps
folder: it is not a Halo disc." The Quest's (XisoExtractor.java) and
the Steam Frame's (steam_frame_xiso.py, RINGFALL_LANGUAGE or the
system's) now take the folder of the device's language, else English's,
else the first, and write its language into maps/language.txt. The game
takes that as the Xbox's language (XGetLanguage) while game.language is
unset, so the menus speak it too. (Reported for ChupathingyCE, another
OpenCE fork, in its issue 145; written anew here.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:30 +02:00
5 changed files with 110 additions and 1 deletions

No files matched your search

+2 -1
View File
@@ -68,7 +68,8 @@ of the game.
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
original Xbox**, made from **a disc you own**. Any region and version
works.
works. The game plays in the disc's language; on the European disc in
Spanish and Italian it takes your headset's language (else the first).
This project does not include, link to or help find game files. Please
do not ask for or share disc images in issues or discussions; such posts
+1
View File
@@ -27,6 +27,7 @@ named where code is taken over.
| Bots | Bots in multiplayer: on foot (paths, items, objectives), in vehicles and at turrets; they make room for a joining person |
| Voice chat | Push to talk on any button (`vr.button_talk`, BUTTONS page; the free hand's grip by default, sending nothing while it reaches for the foregrip or the wheel), open mic, off (2026-10-13) |
| Builds | The Android build's assembly conversion (`tools/android_asm_convert.py`) keeps string data as it is: strings and tables starting with "_" or holding \" lost a "_" (Opus's pulse cache among them: the Steam Frame's crashes in online games while talking), 2026-10-13 |
| Languages | The European disc in Spanish and Italian (a maps folder for each, `maps_es` and `maps_it`, none called `maps`) imported on the Quest and the Steam Frame: the headset's language's, its language the game's and the menus' (`maps/language.txt`, unless `game.language` is set), 2026-10-17; not yet tried with that disc |
| Controls | Left-handed mode (`vr.handedness`): the weapon in the left hand, the arms and grips mirrored, sticks and buttons mirrored on both controllers (`vr.mirror_controls`), the assault rifle's counter read the right way round (2026-10-06) |
| Graphics | Presets low / medium / high / max (`vr.graphics`), refresh rate and resolution in the VR options, all kept over a restart (2026-10-06) |
| Arms and hands | Both arms by IK from shoulders that follow the torso, the elbow led by the hand's roll; fingers from trigger, grip and touch; `vr.body` arms / hands / off, `vr.fingers` (2026-10-06) |
@@ -9,6 +9,7 @@ import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Copies the maps folder out of an Xbox disc image (an "xiso", or a whole
@@ -29,6 +30,12 @@ import java.util.List;
* The files are written to maps.partial first and moved into maps once they
* all are, so an interrupted extraction never leaves maps/ui.map behind.
*
* A European disc in two languages (Spanish and Italian) has no maps folder
* but one for each, maps_es and maps_it: the device's language's is taken,
* else English's, else the first, and its language written to
* maps/language.txt, which the game asks the Xbox for (game.language unset,
* port/linux/src/xbox_xapi.c): the menus in it too.
*
* Some parts of this code are copyright in@fishtank.com. This product
* includes software developed by in &lt;in@fishtank.com&gt;.
*
@@ -225,6 +232,28 @@ final class XisoExtractor {
if (entry.name.equalsIgnoreCase("maps"))
maps = entry;
}
String language = null;
if (maps == null) {
String wanted = Locale.getDefault().getLanguage();
Entry english = null, first = null;
for (Entry entry : directories) {
String code = mapsLanguage(entry.name);
if (code == null)
continue;
if (code.equals(wanted))
maps = entry;
if (code.equals("en"))
english = entry;
if (first == null)
first = entry;
}
if (maps == null)
maps = english != null ? english : first;
if (maps != null)
language = mapsLanguage(maps.name);
}
if (maps == null)
throw new ExtractException("The disc image has no maps folder: it is not a Halo disc.");
@@ -286,5 +315,22 @@ final class XisoExtractor {
throw new ExtractException("Could not move " + file.name + " into " + finished + ".");
}
partial.delete();
File languageFile = new File(finished, "language.txt");
languageFile.delete();
if (language != null) {
try (FileOutputStream out = new FileOutputStream(languageFile)) {
out.write((language + "\n").getBytes(StandardCharsets.US_ASCII));
} catch (IOException e) {
throw new ExtractException("Could not write " + languageFile + " (is the storage full?).");
}
}
}
/** a language's maps folder's language ("maps_it": "it"), else null */
static String mapsLanguage(String name) {
if (name.length() != 7 || !name.regionMatches(true, 0, "maps_", 0, 5))
return null;
String code = name.substring(5).toLowerCase(Locale.ROOT);
return Character.isLetter(code.charAt(0)) && Character.isLetter(code.charAt(1)) ? code : null;
}
}
+31
View File
@@ -138,6 +138,13 @@ def walk(table, offset, depth, directories, out, visited):
walk(table, right, depth + 1, directories, out, visited)
def maps_language(name):
"""a language's maps folder's language ("maps_it": "it"), else None"""
if len(name) == 7 and name[:5].lower() == "maps_" and name[5:].isalpha():
return name[5:].lower()
return None
def extract(image_path, destination):
with open(image_path, "rb") as image:
partition, root_sector, root_size = find_volume(image)
@@ -148,6 +155,24 @@ def extract(image_path, destination):
directories = []
walk(table, 0, 0, True, directories, [0])
maps = [entry for entry in directories if entry[0].lower() == "maps"]
# (a European disc in two languages, Spanish and Italian, has a maps
# folder for each, maps_es and maps_it: the one for the language
# asked for (RINGFALL_LANGUAGE, else the system's), else English's,
# else the first; its language goes into maps/language.txt, which the
# game takes as game.language's when that is unset)
language = None
if not maps:
languages = [(maps_language(entry[0]), entry) for entry in directories if maps_language(entry[0])]
wanted = (os.environ.get("RINGFALL_LANGUAGE") or os.environ.get("LC_ALL") or
os.environ.get("LC_MESSAGES") or os.environ.get("LANG") or "")[:2].lower()
for preferred in (wanted, "en"):
chosen = [pair for pair in languages if pair[0] == preferred]
if chosen:
break
chosen = chosen or languages[:1]
if chosen:
language, entry = chosen[0]
maps = [entry]
if not maps:
raise ExtractError("The disc image has no maps folder: it is not a Halo disc.")
@@ -197,6 +222,12 @@ def extract(image_path, destination):
for name, _, _ in files:
os.replace(os.path.join(partial, name), os.path.join(finished, name))
os.rmdir(partial)
language_file = os.path.join(finished, "language.txt")
if os.path.exists(language_file):
os.remove(language_file)
if language:
with open(language_file, "w") as out:
out.write(language + "\n")
except OSError as error:
raise ExtractError("Could not move the maps into %s (%s)." % (finished, error.strerror))
+30
View File
@@ -42,10 +42,40 @@ DWORD WINAPI XLaunchNewImageA(LPCSTR image_path, PLAUNCH_DATA launch_data)
exit(EXIT_SUCCESS);
}
/* port: a European disc in two languages has a maps folder for each, and
the Android and Steam Frame disc image setups (XisoExtractor.java,
steam_frame_xiso.py) write the language of the one they took into
maps/language.txt: the Xbox's language, unless game.language says another */
static const char *maps_language(void)
{
static char language[4];
static BOOL read;
if (!read)
{
char path[MAX_PATH];
FILE *file;
read = TRUE;
snprintf(path, sizeof(path), "%s/maps/language.txt", platform_data_root());
if ((file = fopen(path, "r")) != NULL)
{
if (!fgets(language, sizeof(language), file))
language[0] = '\0';
language[2] = '\0';
fclose(file);
}
}
return language;
}
DWORD WINAPI XGetLanguage(void)
{
const char *language = config_string("game.language");
if (!*language)
language = maps_language();
if (*language)
{
if (!strncmp(language, "ja", 2)) return XC_LANGUAGE_JAPANESE;