From d6125d0d3a8b1cb0e0eabc91e06328d540b09504 Mon Sep 17 00:00:00 2001 From: lxpollitt <630494+lxpollitt@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:01:47 +0100 Subject: [PATCH 1/3] Housekeeping: ignore .wrangler cache and make gradlew executable - Add .wrangler/ (Cloudflare Workers local cache) to .gitignore so it doesn't clutter working trees when using wrangler dev. - Flip gradlew's mode bit from 0644 to 0755 so a fresh clone on a Unix-like system can run ./gradlew without a chmod step first. --- .gitignore | 3 +++ gradlew | 0 2 files changed, 3 insertions(+) mode change 100644 => 100755 gradlew diff --git a/.gitignore b/.gitignore index 8ea0457..9dd96f9 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ www-test/ ## TeaVM: # Not sure yet... +## Cloudflare Wrangler: +.wrangler/ + ## IntelliJ, Android Studio: .idea/ *.ipr diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 8c816a9113c66ec1df5ba4b18f5e12ce7baf854d Mon Sep 17 00:00:00 2001 From: lxpollitt <630494+lxpollitt@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:37:26 +0100 Subject: [PATCH 2/3] Add ROM selection: Settings dialog and programs.json rom field Adds the ability to choose between Oric ROMs (Atmos BASIC 1.1 default, Oric-1 BASIC 1.0). A settings cog on the home screen opens a dialog for the user's default ROM, applied when loading local files (drag-drop / file picker). Curated entries in programs.json can also declare their required ROM via a new optional "rom" field. - New RomConfig class centralises ROM Option list and the resolveRom() policy used by all three platforms. - New optional AppConfigItem.rom field for programs.json entries. - New DialogHandler.promptForOption, implemented natively on all three platforms: HTML '; options.forEach((option) => { - template += ``; + const selected = option === selectedOption ? ' selected' : ''; + template += ``; }); template += ''; const settings = Object.assign({}, { message: title, template: template}); diff --git a/html/webapp/styles.css b/html/webapp/styles.css index f7fc567..7ce6d2e 100644 --- a/html/webapp/styles.css +++ b/html/webapp/styles.css @@ -175,7 +175,12 @@ a { :where([data-component*="dialog"] [data-ref="message"]) { font-size: var(--dlg-message-fz, 1.25em); + font-weight: bold; margin-block-end: var(--dlg-gap); + /* has browser-default padding-inline that shifts it inward + relative to the fieldset's content; zero it out so the message aligns + with the rest of the dialog body. */ + padding-inline: 0; } :where([data-component*="dialog"] [data-ref="template"]:not(:empty)) { @@ -183,6 +188,29 @@ a { width: 100%; } +/* Form controls inside dialog templates (e.g. the ROM picker). */ +:where([data-component*="dialog"] [data-ref="template"] label) { + display: block; + font-weight: bold; + margin-block-end: 0.5em; +} + +:where([data-component*="dialog"] [data-ref="template"] select) { + width: 100%; + padding: 0.5em; + font-family: inherit; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 0.25em; + background-color: #fff; +} + +/* Give dialogs a sensible minimum width so short-content dialogs + (e.g. the Settings ROM picker) don't collapse into a narrow box. */ +:where([data-component*="dialog"]) { + --dlg-mis: min(28em, calc(100vw - 2em)); +} + /* hack for Firefox */ @-moz-document url-prefix() { [data-component="no-dialog"]:not([hidden]) { diff --git a/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java b/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java index 56c1887..c54d18f 100644 --- a/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java +++ b/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopDialogHandler.java @@ -235,6 +235,27 @@ else if (button == clearButton) { dialogOpen = false; } + @Override + public void promptForOption(final String title, final String message, final String[] options, + final String currentSelection, final TextInputResponseHandler textInputResponseHandler) { + Gdx.app.postRunnable(new Runnable() { + @Override + public void run() { + dialogOpen = true; + String dialogTitle = (title != null && !title.isEmpty()) ? title : "JOric"; + Object result = JOptionPane.showInputDialog(null, message, dialogTitle, + JOptionPane.QUESTION_MESSAGE, null, options, currentSelection); + dialogOpen = false; + + if (result != null) { + textInputResponseHandler.inputTextResult(true, result.toString()); + } else { + textInputResponseHandler.inputTextResult(false, null); + } + } + }); + } + @Override public boolean isDialogOpen() { return dialogOpen; diff --git a/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopJOricRunner.java b/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopJOricRunner.java index e3c78e1..c577ae3 100644 --- a/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopJOricRunner.java +++ b/lwjgl3/src/main/java/emu/joric/lwjgl3/DesktopJOricRunner.java @@ -12,6 +12,7 @@ import emu.joric.MachineType; import emu.joric.PixelData; import emu.joric.Program; +import emu.joric.RomConfig; import emu.joric.config.AppConfigItem; import emu.joric.cpu.Cpu6502; import emu.joric.memory.RamType; @@ -55,7 +56,9 @@ private void runProgram(AppConfigItem appConfigItem, Program program) { machine = new Machine(psg, keyboardMatrix, pixelData); // Load the ROM files. - byte[] basicRom = Gdx.files.internal("roms/basic11b.rom").readBytes(); + RomConfig.Option romOpt = RomConfig.resolveRom( + appConfigItem, Gdx.app.getPreferences("joric.preferences")); + byte[] basicRom = Gdx.files.internal("roms/" + romOpt.filename).readBytes(); byte[] microdiscRom = Gdx.files.internal("roms/microdis.rom").readBytes(); machine.init(basicRom, microdiscRom, program, From dcf346ac855d9c9907c9e5fdcebee62dd5b264aa Mon Sep 17 00:00:00 2001 From: lxpollitt <630494+lxpollitt@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:39:39 +0100 Subject: [PATCH 3/3] Add ?rom= URL parameter for per-launch ROM selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows URLs like https://oric.games/?rom=oric1#/basic or https://oric.games/?rom=oric1&url=... to request a specific ROM for the program being launched. Primarily for sharing links to games that need a specific Oric model. The parameter applies only on direct-load URLs (hash route or ?url=); ?rom= on a bare home screen URL is ignored. Unknown values silently fall back to the default. - captureRomParamFromUrl() in GwtJOricRunner reads ?rom= at page load when a direct-load indicator is present. - RomConfig.setUrlRomParam() / takeUrlRomParam() provide the capture / consume API (lenient about trailing-slash typos). - resolveRom() consults the URL param first, ahead of the curator's rom field and the persisted preference. - clearUrl() strips ?rom= when the user exits back to home (matches existing ?url= cleanup). - README updated to document the parameter. Also fixes a pre-existing bug in the URL rebuild during program start. The old conditional added a '/' before '#' when the URL didn't end in '/', which was correct for URLs without a query string but corrupted query values when one was present (the '/' would be concatenated into the last parameter). setPath(\"/\") guarantees the path-boundary '/' is already in place, so the conditional is unnecessary — simplified to always append \"#/\". --- README.md | 4 ++ .../java/emu/joric/gwt/GwtJOricRunner.java | 46 ++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d56644e..1b0102d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ The UI of JOric has been designed primarily with mobile devices in mind, so give - e.g. [https://oric.games/?url=https://defence-force.org/files/space1999-en.zip](https://oric.games/?url=https://defence-force.org/files/space1999-en.zip) - Support for loading games attached to forum posts: - e.g. [https://oric.games/?url=https://forum.defence-force.org/download/file.php?id=4084](https://oric.games/?url=https://forum.defence-force.org/download/file.php?id=4084) +- Support for specifying the ROM to use for individual game loads via a `?rom=` request parameter: + - e.g. [https://oric.games/?rom=oric1#/basic](https://oric.games/?rom=oric1#/basic) + - e.g. `https://oric.games/?rom=oric1&url=https://example.com/my-oric1-game.tap` + - Valid values are `atmos` (BASIC 1.1, the default) and `oric1` (BASIC 1.0). - Being a PWA (Progressive Web App), it can be installed locally to your device! - And it also comes as a standalone Java app, for those who prefer Java. diff --git a/html/src/main/java/emu/joric/gwt/GwtJOricRunner.java b/html/src/main/java/emu/joric/gwt/GwtJOricRunner.java index 5812cc6..199cf1c 100644 --- a/html/src/main/java/emu/joric/gwt/GwtJOricRunner.java +++ b/html/src/main/java/emu/joric/gwt/GwtJOricRunner.java @@ -45,10 +45,33 @@ public class GwtJOricRunner extends JOricRunner { */ public GwtJOricRunner(KeyboardMatrix keyboardMatrix, PixelData pixelData) { super(keyboardMatrix, pixelData, null); - + psg = new GwtAYPSG(this); - + registerPopStateEventHandler(); + captureRomParamFromUrl(); + } + + /** + * If the page was loaded with a ?rom= URL parameter AND the URL is also + * directly loading a specific program (via ?url= or a hash route like + * #/basic), store the requested ROM id for the program about to + * be launched. The parameter is ignored when the URL has no direct-load + * indicator (e.g. the plain home screen). + */ + private void captureRomParamFromUrl() { + String romParam = Window.Location.getParameter("rom"); + if (romParam == null || romParam.isEmpty()) { + return; + } + String urlParam = Window.Location.getParameter("url"); + String hash = Window.Location.getHash(); + boolean directLoad = + (urlParam != null && !urlParam.isEmpty()) || + (hash != null && !hash.trim().isEmpty()); + if (directLoad) { + RomConfig.setUrlRomParam(romParam); + } } private native void registerPopStateEventHandler() /*-{ @@ -81,20 +104,18 @@ private void onPopState(Event e) { @Override public void start(AppConfigItem appConfigItem) { // Do not change the URL if joric was invoked with "url" request param. - if ((Window.Location.getParameter("url") == null) && + if ((Window.Location.getParameter("url") == null) && (!"Adhoc Oric Program".equals(appConfigItem.getName()))) { - // The URL Builder doesn't add a / before the #, so we do this ourselves. + // setPath normalises the URL to end with a '/' in cases that need them + // such as a bare "http://host". The '#' fragment can be appended + // directly without any extra '/'s to give a well formed URL + // (including for example in the case where a query string is present). String newURL = Window.Location.createUrlBuilder().setPath("/").setHash(null).buildString(); - if (newURL.endsWith("/")) { - newURL += "#/"; - } else { - newURL += "/#/"; - } - newURL += slugify(appConfigItem.getName()); - + newURL += "#/" + slugify(appConfigItem.getName()); + updateURLWithoutReloading(newURL); } - + GwtProgramLoader programLoader = new GwtProgramLoader(); programLoader.fetchProgram(appConfigItem, p -> createWorker(appConfigItem, p)); } @@ -263,6 +284,7 @@ private void clearUrl() { .setPath("/") .setHash(null) .removeParameter("url") + .removeParameter("rom") .buildString(); updateURLWithoutReloading(newURL); }