Added RS Games Linux client.
This commit is contained in:
1 parent
17c1f0f77d
commit
fbafaed846
8 files changed
+271
-4
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# Linux Game Manager Core Map
|
# Linux Game Manager Core Map
|
||||||
|
|
||||||
Last refreshed: 2026-10-30
|
Last refreshed: 2026-10-31
|
||||||
|
|
||||||
## Top-Level Structure
|
## Top-Level Structure
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ Last refreshed: 2026-10-30
|
|||||||
|
|
||||||
## Catalog Snapshot
|
## Catalog Snapshot
|
||||||
|
|
||||||
- Installers: 49 (`.install/*.sh`)
|
- Installers: 50 (`.install/*.sh`)
|
||||||
- Launcher definitions: 49 (`.launch/*.game`)
|
- Launcher definitions: 50 (`.launch/*.game`)
|
||||||
- Launcher runnable entries: 11 (`.launch/*.sh`, both symlinks and files; varies with local installations)
|
- Launcher runnable entries: 38 (`.launch/*.sh`, both symlinks and files; varies with local installations)
|
||||||
- Update scripts: 11 (`.update/*.sh`)
|
- Update scripts: 11 (`.update/*.sh`)
|
||||||
|
|
||||||
Regenerate this snapshot with:
|
Regenerate this snapshot with:
|
||||||
@@ -35,6 +35,7 @@ python3 .codex/skills/linux-game-manager-dev/scripts/audit_game_catalog.py
|
|||||||
- Monkey Spank clones its required submodules recursively and stops installation if cloning fails.
|
- Monkey Spank clones its required submodules recursively and stops installation if cloning fails.
|
||||||
- Shared downloads try `curl` first, fall back to `wget` when available, validate archive payloads before install, and reject obvious HTML error pages for unknown file types.
|
- Shared downloads try `curl` first, fall back to `wget` when available, validate archive payloads before install, and reject obvious HTML error pages for unknown file types.
|
||||||
- `download_named()` shares the same cache validation path as `download()`, so resolved asset URLs must still produce a valid payload for their file extension.
|
- `download_named()` shares the same cache validation path as `download()`, so resolved asset URLs must still produce a valid payload for their file extension.
|
||||||
|
- RS Games installs the fixed 2.3 x86_64 archive under `${installPath}/RS_Games` and launches `./rsg` from that directory. The archive omits `speechd`; the installer requires host Python Speech Dispatcher bindings and copies their Python sources into `_internal/speechd`. Run `.files/rs-games-alsa.py` to silently test default-device initialization with the bundled library. Keep a working bundle untouched. Only set it aside as `libasound.so.2.bundled` when bundled initialization fails and system ALSA passes the same check; restore it if the system check fails. Inconclusive initial probes retain the bundle and report diagnostics. The bundled library has incompatible plugin paths and symbol versions on Arch Linux. Run `python3 -B -m unittest discover -s tests` for ALSA selection/restoration and path-resolution regressions. Run `.files/rs-games-audio.py` after extraction to correct non-Windows file-path encoding in `sound_lib.stream`; the helper renames its frozen PYZ entry so the corrected filesystem source is used and preserves `rsg.lgm-original`. Confirm with the frozen importer, since direct BASS decoding bypasses the failing wrapper. The repair also installs `sound_lib/lgm_paths.py` for per-call case-insensitive sound lookup: exact paths win, otherwise each component must have one case-insensitive match; missing or ambiguous paths stay unchanged. Downloads are never renamed and misses are not cached. Validate with `python3 -B -m unittest discover -s tests -p "test_rs_games_paths.py"` and the actual frozen importer. Client logs are in `~/.ZGP/errors.log`.
|
||||||
- Wickets resolves the latest stable GitHub release on each install/update, extracts its Linux x64 tarball into staging, and overlays `${installPath}/Wickets` while preserving `saves`. A `.lgm-version` marker records the release installed by the manager.
|
- Wickets resolves the latest stable GitHub release on each install/update, extracts its Linux x64 tarball into staging, and overlays `${installPath}/Wickets` while preserving `saves`. A `.lgm-version` marker records the release installed by the manager.
|
||||||
- If `.launch/<game>.game` exists and `.launch/<game>.sh` does not, the manager creates a symlink using the repository root, even if the installer changed directories.
|
- If `.launch/<game>.game` exists and `.launch/<game>.sh` does not, the manager creates a symlink using the repository root, even if the installer changed directories.
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ python3 .codex/skills/linux-game-manager-dev/scripts/audit_game_catalog.py
|
|||||||
- Clone games with required submodules using `git clone --recurse-submodules` and stop installation on clone failure.
|
- Clone games with required submodules using `git clone --recurse-submodules` and stop installation on clone failure.
|
||||||
- Launch games that load relative assets from their own directory. Monkey Spank uses `${installPath}/monkeyspank` as its working directory and removal root.
|
- Launch games that load relative assets from their own directory. Monkey Spank uses `${installPath}/monkeyspank` as its working directory and removal root.
|
||||||
- Wickets shares install/update logic in `.includes/wickets.sh`; its versioned tarball has one top-level directory, and portable saves must survive updates. Always refresh GitHub release metadata rather than reusing a cached latest-release response.
|
- Wickets shares install/update logic in `.includes/wickets.sh`; its versioned tarball has one top-level directory, and portable saves must survive updates. Always refresh GitHub release metadata rather than reusing a cached latest-release response.
|
||||||
|
- RS Games 2.3 has a single `rsg/` archive root; strip that component into `${installPath}/RS_Games` and launch `./rsg` from that directory. Require `python-speechd:speechd` and copy its Python sources into `_internal/speechd`, since the frozen runtime cannot import host site-packages and the release omits these bindings. Run `.files/rs-games-alsa.py` to silently test default-device initialization with the bundled library. Keep a working bundle untouched. Only set it aside as `libasound.so.2.bundled` when bundled initialization fails and system ALSA passes the same check; restore it if the system check fails. Inconclusive initial probes retain the bundle and report diagnostics. The bundled library has incompatible plugin paths and symbol versions on Arch Linux. Run `python3 -B -m unittest discover -s tests` for ALSA selection/restoration and path-resolution regressions. Run `.files/rs-games-audio.py` after extraction to correct non-Windows file-path encoding in `sound_lib.stream`; the helper renames its frozen PYZ entry so the corrected filesystem source is used and preserves `rsg.lgm-original`. Confirm with the frozen importer, since direct BASS decoding bypasses the failing wrapper. The repair also installs `sound_lib/lgm_paths.py` for per-call case-insensitive sound lookup: exact paths win, otherwise each component must have one case-insensitive match; missing or ambiguous paths stay unchanged. Downloads are never renamed and misses are not cached. Validate with `python3 -B -m unittest discover -s tests -p "test_rs_games_paths.py"` and the actual frozen importer. Client logs are in `~/.ZGP/errors.log`.
|
||||||
- Avoid introducing unnecessary colorized output.
|
- Avoid introducing unnecessary colorized output.
|
||||||
- For edited bash scripts, run shellcheck and fix all errors.
|
- For edited bash scripts, run shellcheck and fix all errors.
|
||||||
- If `shellcheck` is not installed, prompt the user to install it first (see `references/tooling-prereqs.md`).
|
- If `shellcheck` is not installed, prompt the user to install it first (see `references/tooling-prereqs.md`).
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Keep bundled ALSA unless the system copy demonstrably fixes audio startup."""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def probe_audio(gamePath):
|
||||||
|
probeEnvironment = os.environ.copy()
|
||||||
|
libraryPath = str(gamePath / "_internal")
|
||||||
|
if probeEnvironment.get("LD_LIBRARY_PATH"):
|
||||||
|
libraryPath += os.pathsep + probeEnvironment["LD_LIBRARY_PATH"]
|
||||||
|
probeEnvironment["LD_LIBRARY_PATH"] = libraryPath
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(Path(__file__).resolve()), "--probe", str(gamePath)],
|
||||||
|
env=probeEnvironment, capture_output=True, text=True, timeout=10, check=False,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as error:
|
||||||
|
return 2, str(error)
|
||||||
|
return result.returncode, (result.stdout + result.stderr).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def configure_alsa(gamePath):
|
||||||
|
libraryPath = gamePath / "_internal/libasound.so.2"
|
||||||
|
if not libraryPath.exists():
|
||||||
|
print("RS Games has no active bundled ALSA library; leaving it unchanged.")
|
||||||
|
return
|
||||||
|
probeStatus, probeDetails = probe_audio(gamePath)
|
||||||
|
if probeStatus == 0:
|
||||||
|
print("RS Games bundled ALSA initializes successfully; keeping it.")
|
||||||
|
return
|
||||||
|
if probeStatus != 1:
|
||||||
|
print(f"Could not verify RS Games audio; keeping bundled ALSA.\n{probeDetails}")
|
||||||
|
return
|
||||||
|
|
||||||
|
backupPath = libraryPath.with_name("libasound.so.2.bundled")
|
||||||
|
if backupPath.exists() and backupPath.read_bytes() != libraryPath.read_bytes():
|
||||||
|
raise ValueError("Existing ALSA backup differs; refusing to overwrite")
|
||||||
|
libraryPath.replace(backupPath)
|
||||||
|
systemWorks = False
|
||||||
|
try:
|
||||||
|
systemStatus, systemDetails = probe_audio(gamePath)
|
||||||
|
systemWorks = systemStatus == 0
|
||||||
|
finally:
|
||||||
|
if not systemWorks:
|
||||||
|
shutil.copy2(backupPath, libraryPath)
|
||||||
|
if systemWorks:
|
||||||
|
print("RS Games bundled ALSA failed; verified system ALSA and preserved the bundled copy.")
|
||||||
|
else:
|
||||||
|
print("Neither ALSA setup could be verified; restored bundled ALSA.")
|
||||||
|
print(f"Bundled: {probeDetails}\nSystem: {systemDetails}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_probe(gamePath):
|
||||||
|
try:
|
||||||
|
bassLibrary = ctypes.CDLL(str(gamePath / "_internal/sound_lib/lib/x64/libbass.so"))
|
||||||
|
bassLibrary.BASS_Init.argtypes = [
|
||||||
|
ctypes.c_int, ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p,
|
||||||
|
]
|
||||||
|
if not bassLibrary.BASS_Init(1, 44100, 0, None, None):
|
||||||
|
errorCode = bassLibrary.BASS_ErrorGetCode()
|
||||||
|
print(f"Default audio device initialization failed (BASS error {errorCode}).")
|
||||||
|
return 1 if errorCode == 3 else 2
|
||||||
|
bassLibrary.BASS_Free()
|
||||||
|
return 0
|
||||||
|
except OSError as error:
|
||||||
|
print(f"Audio library could not load: {error}")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) == 3 and sys.argv[1] == "--probe":
|
||||||
|
raise SystemExit(run_probe(Path(sys.argv[2]).resolve()))
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
raise SystemExit("Usage: rs-games-alsa.py GAME_DIRECTORY")
|
||||||
|
try:
|
||||||
|
configure_alsa(Path(sys.argv[1]).resolve())
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
raise SystemExit(f"RS Games ALSA setup failed: {error}") from error
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Repair Linux file paths in the frozen RS Games 2.3 sound wrapper."""
|
||||||
|
|
||||||
|
import marshal
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def repair_audio(gamePath):
|
||||||
|
executablePath = gamePath / "rsg"
|
||||||
|
streamPath = gamePath / "_internal/sound_lib/stream.py"
|
||||||
|
executableData = executablePath.read_bytes()
|
||||||
|
streamData = streamPath.read_bytes()
|
||||||
|
originalBlock = (
|
||||||
|
b'if platform.system() == "Darwin" and file and not mem:\r\n'
|
||||||
|
b' unicode = False\r\n'
|
||||||
|
b' file = file.encode(sys.getfilesystemencoding())'
|
||||||
|
)
|
||||||
|
encodedBlock = (
|
||||||
|
b'if platform.system() != "Windows" and not mem and isinstance(file, (str, bytes, os.PathLike)):\r\n'
|
||||||
|
b' unicode = False\r\n'
|
||||||
|
b' file = os.fsencode(file)'
|
||||||
|
)
|
||||||
|
repairedBlock = (
|
||||||
|
b'if platform.system() != "Windows" and not mem and isinstance(file, (str, bytes, os.PathLike)):\r\n'
|
||||||
|
b' from .lgm_paths import resolve_sound_path\r\n'
|
||||||
|
b' unicode = False\r\n'
|
||||||
|
b' file = os.fsencode(resolve_sound_path(file))'
|
||||||
|
)
|
||||||
|
resolverData = Path(__file__).with_name("rs-games-paths.py").read_bytes()
|
||||||
|
if streamData.count(originalBlock) == 1:
|
||||||
|
repairedStream = streamData.replace(originalBlock, repairedBlock)
|
||||||
|
elif streamData.count(encodedBlock) == 1:
|
||||||
|
repairedStream = streamData.replace(encodedBlock, repairedBlock)
|
||||||
|
elif streamData.count(repairedBlock) == 1:
|
||||||
|
repairedStream = streamData
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected RS Games sound wrapper; refusing to patch")
|
||||||
|
|
||||||
|
# Rename only this module's PYZ entry. PyInstaller then falls back to the
|
||||||
|
# supplied filesystem source, making the corrected stream.py effective.
|
||||||
|
cookiePosition = executableData.rfind(b"MEI\014\013\012\013\016")
|
||||||
|
if cookiePosition < 0:
|
||||||
|
raise ValueError("PyInstaller archive cookie not found")
|
||||||
|
_, packageSize, tocOffset, tocSize, pythonVersion, _ = struct.unpack_from(
|
||||||
|
"!8sIIII64s", executableData, cookiePosition
|
||||||
|
)
|
||||||
|
if pythonVersion != 312:
|
||||||
|
raise ValueError("Expected the Python 3.12 RS Games release")
|
||||||
|
packageStart = cookiePosition + 88 - packageSize
|
||||||
|
entryPosition = packageStart + tocOffset
|
||||||
|
tocEnd = entryPosition + tocSize
|
||||||
|
pyzStart = None
|
||||||
|
pyzEnd = None
|
||||||
|
while entryPosition < tocEnd:
|
||||||
|
entrySize, offset, dataSize, _, compressed, entryType = struct.unpack_from(
|
||||||
|
"!IIIIBc", executableData, entryPosition
|
||||||
|
)
|
||||||
|
if entrySize < 18 or entryPosition + entrySize > tocEnd:
|
||||||
|
raise ValueError("Invalid archive table entry")
|
||||||
|
entryName = executableData[entryPosition + 18:entryPosition + entrySize].rstrip(b"\0")
|
||||||
|
if entryName == b"PYZ.pyz" and entryType == b"z" and not compressed:
|
||||||
|
pyzStart = packageStart + offset
|
||||||
|
pyzEnd = pyzStart + dataSize
|
||||||
|
entryPosition += entrySize
|
||||||
|
if pyzStart is None or executableData[pyzStart:pyzStart + 4] != b"PYZ\0":
|
||||||
|
raise ValueError("Uncompressed PYZ archive not found")
|
||||||
|
pyzTocStart = pyzStart + struct.unpack_from("!i", executableData, pyzStart + 8)[0]
|
||||||
|
if not pyzStart + 12 <= pyzTocStart < pyzEnd <= cookiePosition:
|
||||||
|
raise ValueError("Invalid PYZ table bounds")
|
||||||
|
tableData = executableData[pyzTocStart:pyzEnd]
|
||||||
|
originalName = b"sound_lib.stream"
|
||||||
|
savedName = b"lgm_saved_stream"
|
||||||
|
originalTable = dict(marshal.loads(tableData))
|
||||||
|
if "sound_lib.stream" in originalTable and "lgm_saved_stream" not in originalTable:
|
||||||
|
if tableData.count(originalName) != 1:
|
||||||
|
raise ValueError("Ambiguous sound module table entry")
|
||||||
|
repairedTableData = tableData.replace(originalName, savedName)
|
||||||
|
expectedTable = originalTable.copy()
|
||||||
|
expectedTable["lgm_saved_stream"] = expectedTable.pop("sound_lib.stream")
|
||||||
|
if dict(marshal.loads(repairedTableData)) != expectedTable:
|
||||||
|
raise ValueError("Archive table verification failed")
|
||||||
|
repairedExecutable = executableData[:pyzTocStart] + repairedTableData + executableData[pyzEnd:]
|
||||||
|
elif "lgm_saved_stream" in originalTable and "sound_lib.stream" not in originalTable:
|
||||||
|
repairedExecutable = executableData
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected sound module archive entries")
|
||||||
|
|
||||||
|
if repairedExecutable != executableData:
|
||||||
|
backupPath = executablePath.with_name("rsg.lgm-original")
|
||||||
|
if backupPath.exists() and backupPath.read_bytes() != executableData:
|
||||||
|
raise ValueError("Existing executable backup differs; refusing to overwrite")
|
||||||
|
if not backupPath.exists():
|
||||||
|
shutil.copy2(executablePath, backupPath)
|
||||||
|
streamPath.with_name("lgm_paths.py").write_bytes(resolverData)
|
||||||
|
streamPath.write_bytes(repairedStream)
|
||||||
|
if repairedExecutable != executableData:
|
||||||
|
temporaryPath = executablePath.with_name("rsg.lgm-new")
|
||||||
|
temporaryPath.write_bytes(repairedExecutable)
|
||||||
|
temporaryPath.chmod(executablePath.stat().st_mode)
|
||||||
|
temporaryPath.replace(executablePath)
|
||||||
|
print("RS Games Linux audio file paths repaired.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
raise SystemExit("Usage: rs-games-audio.py GAME_DIRECTORY")
|
||||||
|
try:
|
||||||
|
repair_audio(Path(sys.argv[1]))
|
||||||
|
except (OSError, ValueError, EOFError, TypeError, struct.error) as error:
|
||||||
|
raise SystemExit(f"RS Games audio repair failed: {error}") from error
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Resolve uniquely matching sound paths from Windows-authored game packs."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_sound_path(filePath):
|
||||||
|
originalPath = os.fspath(filePath)
|
||||||
|
if os.path.exists(originalPath):
|
||||||
|
return originalPath
|
||||||
|
|
||||||
|
pathText = os.fsdecode(originalPath)
|
||||||
|
resolvedPath = os.path.sep if os.path.isabs(pathText) else os.curdir
|
||||||
|
for component in pathText.split(os.path.sep):
|
||||||
|
if component in ("", "."):
|
||||||
|
continue
|
||||||
|
candidatePath = os.path.join(resolvedPath, component)
|
||||||
|
if os.path.exists(candidatePath):
|
||||||
|
resolvedPath = candidatePath
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with os.scandir(resolvedPath) as entries:
|
||||||
|
matches = [entry.name for entry in entries if entry.name.casefold() == component.casefold()]
|
||||||
|
except OSError:
|
||||||
|
return originalPath
|
||||||
|
if len(matches) != 1:
|
||||||
|
return originalPath
|
||||||
|
resolvedPath = os.path.join(resolvedPath, matches[0])
|
||||||
|
|
||||||
|
return os.fsencode(resolvedPath) if isinstance(originalPath, bytes) else resolvedPath
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# shellcheck shell=bash disable=SC2154 # installPath and cache are supplied by the manager.
|
||||||
|
check_architecture x86_64
|
||||||
|
check_dependencies python-speechd:speechd
|
||||||
|
download "https://github.com/rsgames-zgp/rsgames-client-releases/releases/download/v2.3/rsg-linux-x86_64.tar.gz"
|
||||||
|
mkdir -p "${installPath}/RS_Games" || exit 1
|
||||||
|
tar -xzf "${cache}/rsg-linux-x86_64.tar.gz" --strip-components=1 -C "${installPath}/RS_Games" || exit 1
|
||||||
|
# Keep bundled ALSA when it works; use system ALSA only after verifying it.
|
||||||
|
python3 "${scriptRoot}/.files/rs-games-alsa.py" "${installPath}/RS_Games" || exit 1
|
||||||
|
chmod +x "${installPath}/RS_Games/rsg" || exit 1
|
||||||
|
# The frozen client omits speechd and cannot import host site-packages.
|
||||||
|
speechdPath="$(python3 -c 'import pathlib, speechd; print(pathlib.Path(speechd.__file__).resolve().parent)')" || exit 1
|
||||||
|
mkdir -p "${installPath}/RS_Games/_internal/speechd" || exit 1
|
||||||
|
cp "${speechdPath}/"*.py "${installPath}/RS_Games/_internal/speechd/" || exit 1
|
||||||
|
python3 "${scriptRoot}/.files/rs-games-audio.py" "${installPath}/RS_Games" || exit 1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# shellcheck shell=bash disable=SC2154 # installPath and fex are supplied by the manager.
|
||||||
|
check_architecture x86_64
|
||||||
|
gamePath="${installPath}/RS_Games"
|
||||||
|
cd "${gamePath}" || exit 1
|
||||||
|
# shellcheck disable=SC2086 # fex intentionally expands to an optional command prefix.
|
||||||
|
run_with_optional_gamemode ${fex}./rsg
|
||||||
@@ -17,6 +17,25 @@ query GitHub for the latest stable release, so versioned download links do not
|
|||||||
need to be tracked manually. Updating preserves the portable `saves` directory
|
need to be tracked manually. Updating preserves the portable `saves` directory
|
||||||
under `~/.local/games/Wickets`. Wickets 1.2.0 also includes its own updater.
|
under `~/.local/games/Wickets`. Wickets 1.2.0 also includes its own updater.
|
||||||
|
|
||||||
|
## RS Games
|
||||||
|
|
||||||
|
Install RS Games 2.3 on x86_64 Linux by selecting RS Games under
|
||||||
|
`./linux-game-manager.sh -i`. The client is installed under
|
||||||
|
`~/.local/games/RS_Games` and can then be launched from the main game menu.
|
||||||
|
The installer requires the host Python Speech Dispatcher bindings (`python-speechd`
|
||||||
|
on Arch Linux or `python3-speechd` on Debian) and copies them into the client,
|
||||||
|
whose Linux release omits them.
|
||||||
|
The installer silently checks audio initialization and keeps bundled ALSA when
|
||||||
|
it works. If it fails and system ALSA passes the same check, the bundled copy
|
||||||
|
is set aside as `_internal/libasound.so.2.bundled`. If audio cannot be verified,
|
||||||
|
the bundled library is retained and the installer reports the problem.
|
||||||
|
The installer also repairs Linux audio file-path handling in the sound wrapper
|
||||||
|
and preserves the original executable as `rsg.lgm-original`.
|
||||||
|
Sound lookup automatically handles capitalization differences in filenames and
|
||||||
|
directories. Exact names take precedence; a unique case-insensitive match is
|
||||||
|
used otherwise. Downloaded files retain their original names.
|
||||||
|
Client errors are logged in `~/.ZGP/errors.log`.
|
||||||
|
|
||||||
## Disabling game entries
|
## Disabling game entries
|
||||||
|
|
||||||
To disable a game entry without deleting its script, set the first line of the script to start with `#//`.
|
To disable a game entry without deleting its script, set the first line of the script to start with `#//`.
|
||||||
|
|||||||
Reference in new issue
Block a user