feat(env): add pluggable world environments
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sat, 12 Sep 2026 09:58:20 +0000 (12:58 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sat, 12 Sep 2026 09:58:20 +0000 (12:58 +0300)
Add an EnvironmentProvider contract so the workspace can open a
pluggable 3D world by name, with discovery through a registry that
pairs built-in providers with ServiceLoader classpath plug-ins.

The --env=<name> flag resolves a provider, reports unknown or
unavailable environments, and lets the workspace own the provider
lifecycle. A built-in grid provider validates the contract without
external data. Test-driver flags are recognized anywhere on the
command line so they can coexist with --env, and start.sh rebuilds the
modules before launching the Fallout 4 environment.

TODO.org
src/main/java/eu/svjatoslav/sixth/core/Main.java
src/main/java/eu/svjatoslav/sixth/env/EnvironmentContext.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/env/EnvironmentProvider.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/env/EnvironmentRegistry.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/env/GridEnvironmentProvider.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/workspace/Workspace.java
start.sh [new file with mode: 0755]

index 6063160..80b3e5d 100644 (file)
--- a/TODO.org
+++ b/TODO.org
@@ -1,3 +1,4 @@
+
 * Put agents in 3D world
 
 - Agent has body that resembles human
   - Kimi K3
 
 
+
+
+* World is built around intent
+
+- you start with the world and you name intent for it
+
+- AI is monitoring what you are doing and is always ready to help
+  - rename space to better capture intent
+
+- you create new (optionally linked) spaces with (sub intents)
+  - you can inherit copy of existing apps into new space or star with empty space
+
+
+
index b0d232e..c93fc1e 100644 (file)
@@ -24,49 +24,96 @@ public class Main {
     private static final long SELFTEST_TIMEOUT_MS = 15_000;
 
     public static void main(final String[] args) throws Exception {
-        if (args.length > 0 && "--clicktest".equals(args[0]))
+        String environmentName = null;
+        for (final String arg : args)
+            if (arg.startsWith("--env="))
+                environmentName = arg.substring("--env=".length());
+
+        if (hasArg(args, "--clicktest"))
             prepareClickTestPage();
 
-        if (args.length > 0 && "--keytest".equals(args[0]))
+        if (hasArg(args, "--keytest"))
             prepareKeyTestPage();
 
-        if (args.length > 0 && "--scrolltest".equals(args[0]))
+        if (hasArg(args, "--scrolltest"))
             prepareScrollTestPage();
 
-        if (args.length > 0 && "--hovertest".equals(args[0]))
+        if (hasArg(args, "--hovertest"))
             prepareHoverTestPage();
 
         final Workspace workspace = new Workspace();
         workspace.open();
 
-        if (args.length > 0 && "--selftest".equals(args[0]))
+        if (environmentName != null)
+            openRequestedEnvironment(workspace, environmentName);
+
+        if (hasArg(args, "--selftest"))
             System.exit(runSelfTest(workspace));
 
-        if (args.length > 0 && "--guitest".equals(args[0]))
+        if (hasArg(args, "--guitest"))
             System.exit(runGuiTest(workspace));
 
-        if (args.length > 0 && "--clicktest".equals(args[0]))
+        if (hasArg(args, "--clicktest"))
             System.exit(runClickTest(workspace));
 
-        if (args.length > 0 && "--keytest".equals(args[0]))
+        if (hasArg(args, "--keytest"))
             System.exit(runKeyTest(workspace));
 
-        if (args.length > 0 && "--scrolltest".equals(args[0]))
+        if (hasArg(args, "--scrolltest"))
             System.exit(runScrollTest(workspace));
 
-        if (args.length > 0 && "--hovertest".equals(args[0]))
+        if (hasArg(args, "--hovertest"))
             System.exit(runHoverTest(workspace));
 
-        if (args.length > 0 && "--headtest".equals(args[0]))
+        if (hasArg(args, "--headtest"))
             System.exit(runHeadTest(workspace));
 
-        if (args.length > 0 && "--headdebug".equals(args[0]))
+        if (hasArg(args, "--headdebug"))
             startHeadDebugLogger(workspace);
 
         Runtime.getRuntime().addShutdownHook(
                 new Thread(workspace::close));
     }
 
+    /**
+     * True when the exact flag appears anywhere on the command line;
+     * test drivers must not be position-sensitive now that --env= can
+     * occupy args[0].
+     */
+    private static boolean hasArg(final String[] args, final String flag) {
+        for (final String arg : args)
+            if (flag.equals(arg))
+                return true;
+        return false;
+    }
+
+    /**
+     * Resolves the --env=<name> provider, checks availability, and opens
+     * it into the running workspace. Exits with status 1 when the name
+     * is unknown or the provider's data files are missing.
+     */
+    private static void openRequestedEnvironment(final Workspace workspace,
+                                                 final String name)
+            throws Exception {
+        final var provider
+                = eu.svjatoslav.sixth.env.EnvironmentRegistry.findByName(name);
+        if (provider == null) {
+            System.out.println("no environment named '" + name + "'");
+            eu.svjatoslav.sixth.env.EnvironmentRegistry
+                    .printAvailableEnvironments();
+            System.exit(1);
+        }
+        if (!provider.isAvailable()) {
+            System.out.println("environment '" + name + "' ("
+                    + provider.getDisplayName() + ") is not available on"
+                    + " this machine — its data files were not found");
+            System.exit(1);
+        }
+        System.out.println("opening environment: "
+                + provider.getDisplayName());
+        workspace.openEnvironment(provider);
+    }
+
     /**
      * Drives the terminal without AWT events: injects bytes straight into
      * the PTY, exactly what a focused keystroke would send.
diff --git a/src/main/java/eu/svjatoslav/sixth/env/EnvironmentContext.java b/src/main/java/eu/svjatoslav/sixth/env/EnvironmentContext.java
new file mode 100644 (file)
index 0000000..a38319b
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Sixth spatial computing environment. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.env;
+
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.gui.FrameListener;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+
+/**
+ * Host services handed to an {@link EnvironmentProvider} when it is
+ * opened. This is deliberately a narrow window into the workspace: the
+ * scene to add shapes to, the camera to place at the world's spawn
+ * point, and the per-frame tick for streaming worlds.
+ */
+public final class EnvironmentContext {
+
+    private final ViewPanel viewPanel;
+
+    public EnvironmentContext(final ViewPanel viewPanel) {
+        this.viewPanel = viewPanel;
+    }
+
+    /**
+     * The root scene graph. The environment adds its shapes here and
+     * keeps references to whatever it must later remove in close().
+     */
+    public ShapeCollection getScene() {
+        return viewPanel.getRootShapeCollection();
+    }
+
+    /**
+     * The workspace camera. An environment typically moves it to its
+     * spawn point in open(); afterwards the user owns it (fly controls,
+     * head tracking, SpaceNavigator).
+     */
+    public Camera getCamera() {
+        return viewPanel.getCamera();
+    }
+
+    /**
+     * Registers a per-frame callback — the streaming world's heartbeat:
+     * "recompute what should exist around the camera." Return true from
+     * the callback when the scene changed and needs a repaint.
+     */
+    public void addFrameListener(final FrameListener listener) {
+        viewPanel.addFrameListener(listener);
+    }
+
+    public void removeFrameListener(final FrameListener listener) {
+        viewPanel.removeFrameListener(listener);
+    }
+
+    /**
+     * Requests a repaint after scene changes made outside a frame
+     * callback (e.g. from a background loader thread).
+     */
+    public void requestRepaint() {
+        viewPanel.repaintDuringNextViewUpdate();
+    }
+
+    /**
+     * Escape hatch to the underlying view panel. Prefer the narrower
+     * methods above; this exists for things like reading viewport size.
+     */
+    public ViewPanel getViewPanel() {
+        return viewPanel;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/env/EnvironmentProvider.java b/src/main/java/eu/svjatoslav/sixth/env/EnvironmentProvider.java
new file mode 100644 (file)
index 0000000..13881b8
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sixth spatial computing environment. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.env;
+
+/**
+ * A pluggable 3D environment ("world provider") for the Sixth workspace:
+ * a game world, a map, or a procedural scene that the workspace panels
+ * live inside of. Today the wallpaper is a Fallout 4 level, tomorrow a
+ * Quake map, the day after a 3D fractal — the workspace does not care,
+ * it only knows this interface.
+ *
+ * <p>Implementations are discovered in two ways:</p>
+ * <ul>
+ *   <li>built-in providers, registered directly in
+ *       {@link EnvironmentRegistry};</li>
+ *   <li>external providers on the classpath, discovered via
+ *       {@link java.util.ServiceLoader} — a provider jar declares itself in
+ *       {@code META-INF/services/eu.svjatoslav.sixth.env.EnvironmentProvider}.
+ *       This is how separate environment projects (e.g.
+ *       sixth-environment-fo4) plug in without Sixth depending on them.</li>
+ * </ul>
+ *
+ * <p>Selection: {@code sixth --env=<name>} picks a provider by
+ * {@link #getName()}.</p>
+ *
+ * <p>Lifecycle: {@link #open(EnvironmentContext)} is called once after the
+ * workspace window exists. The provider builds its initial scene into
+ * {@link EnvironmentContext#getScene()}, positions the camera via
+ * {@link EnvironmentContext#getCamera()}, and — for streaming worlds —
+ * registers a per-frame callback via
+ * {@link EnvironmentContext#addFrameListener} whose job is "recompute what
+ * should exist around the camera, add/remove shapes accordingly."
+ * {@link #close()} must unregister listeners and release resources.</p>
+ *
+ * <p>What an environment is NOT: it never touches the engine internals,
+ * never creates windows, and never interferes with workspace panels —
+ * it only adds shapes to the shared scene and moves nothing it does not
+ * own.</p>
+ */
+public interface EnvironmentProvider {
+
+    /**
+     * Short unique selector used on the command line, e.g. "fallout4",
+     * "quake1", "fractal". Lower case, no spaces.
+     */
+    String getName();
+
+    /**
+     * Human-readable name, e.g. "Fallout 4 — the Commonwealth".
+     */
+    String getDisplayName();
+
+    /**
+     * True when this provider can actually run on this machine — for
+     * game worlds this means the game data files were found. Providers
+     * that need no external data (procedural worlds) always return true.
+     */
+    boolean isAvailable();
+
+    /**
+     * Builds the initial scene and starts any background streaming.
+     * Called once, after the workspace window exists.
+     *
+     * @param context host services: the scene, the camera, frame ticks
+     * @throws Exception if the environment cannot be built (for game
+     *                   worlds: data files unreadable/corrupt)
+     */
+    void open(EnvironmentContext context) throws Exception;
+
+    /**
+     * Stops background work and removes the environment's shapes from
+     * the scene. Must tolerate being called after a failed open().
+     */
+    void close();
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/env/EnvironmentRegistry.java b/src/main/java/eu/svjatoslav/sixth/env/EnvironmentRegistry.java
new file mode 100644 (file)
index 0000000..14993e8
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sixth spatial computing environment. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.env;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * Finds the environments that can run on this machine: built-in
+ * providers plus any declared on the classpath via
+ * {@code META-INF/services/eu.svjatoslav.sixth.env.EnvironmentProvider}
+ * (the plug-in mechanism used by separate environment projects such as
+ * sixth-environment-fo4).
+ */
+public final class EnvironmentRegistry {
+
+    private EnvironmentRegistry() {
+    }
+
+    /**
+     * All discovered providers, available or not. Built-ins first, then
+     * classpath plug-ins; keyed by name, later duplicates lose.
+     */
+    public static List<EnvironmentProvider> discoverProviders() {
+        final Map<String, EnvironmentProvider> providers
+                = new LinkedHashMap<>();
+        providers.put("grid", new GridEnvironmentProvider());
+        for (final EnvironmentProvider provider
+                : ServiceLoader.load(EnvironmentProvider.class))
+            providers.putIfAbsent(provider.getName(), provider);
+        return new ArrayList<>(providers.values());
+    }
+
+    /**
+     * Looks up a provider by its command-line name; null when no
+     * provider with that name is on the classpath.
+     */
+    public static EnvironmentProvider findByName(final String name) {
+        for (final EnvironmentProvider provider : discoverProviders())
+            if (provider.getName().equals(name))
+                return provider;
+        return null;
+    }
+
+    /**
+     * Prints the known providers and their availability, for CLI help
+     * and for the error message when a requested provider is missing.
+     */
+    public static void printAvailableEnvironments() {
+        System.out.println("known environments:");
+        for (final EnvironmentProvider provider : discoverProviders())
+            System.out.println("  " + provider.getName()
+                    + (provider.isAvailable() ? "" : " (unavailable)")
+                    + " — " + provider.getDisplayName());
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/env/GridEnvironmentProvider.java b/src/main/java/eu/svjatoslav/sixth/env/GridEnvironmentProvider.java
new file mode 100644 (file)
index 0000000..164b602
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sixth spatial computing environment. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.env;
+
+import eu.svjatoslav.sixth.e3d.geometry.Rectangle;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.Grid2D;
+
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.hex;
+
+/**
+ * Built-in proof environment: a huge ground grid under the workspace.
+ * Exists to validate the provider contract (discovery, lifecycle, camera
+ * placement) without any external data — and as the reference
+ * implementation for how little a working provider can be.
+ */
+public class GridEnvironmentProvider implements EnvironmentProvider {
+
+    @Override
+    public String getName() {
+        return "grid";
+    }
+
+    @Override
+    public String getDisplayName() {
+        return "Ground grid (built-in test environment)";
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+    @Override
+    public void open(final EnvironmentContext context) {
+        // ground plane at y=0, camera 150 units above it (negative Y is
+        // up in engine coordinates), looking slightly down
+        final Transform transform = Transform.fromAngles(0, 0, 0, 0,
+                Math.PI / 2, 0);
+        context.getScene().addShape(new Grid2D(transform,
+                new Rectangle(20000), 40, 40,
+                new LineAppearance(10, hex("5a5a3a"))));
+        context.getCamera().getTransform().set(0, -150, -300, 0, -0.4, 0);
+        context.requestRepaint();
+    }
+
+    @Override
+    public void close() {
+        // keeps no references and no background work — nothing to undo
+    }
+}
index 6468596..ed98493 100644 (file)
@@ -48,11 +48,24 @@ public class Workspace {
     private final ViewFrame viewFrame;
     private final List<TerminalPanel> terminalPanels = new ArrayList<>();
     private FirefoxPanel firefoxPanel;
+    private eu.svjatoslav.sixth.env.EnvironmentProvider environment;
 
     public Workspace() {
         viewFrame = new ViewFrame("Sixth");
     }
 
+    /**
+     * Attaches a pluggable environment (game world, procedural scene)
+     * after open(); the workspace then owns its lifecycle.
+     */
+    public void openEnvironment(
+            final eu.svjatoslav.sixth.env.EnvironmentProvider provider)
+            throws Exception {
+        environment = provider;
+        provider.open(new eu.svjatoslav.sixth.env.EnvironmentContext(
+                viewFrame.getViewPanel()));
+    }
+
     /**
      * Builds the scene and shows the window.
      *
@@ -158,6 +171,8 @@ public class Workspace {
      * Shuts down all background shell processes.
      */
     public void close() {
+        if (environment != null)
+            environment.close();
         for (final TerminalPanel terminalPanel : terminalPanels)
             terminalPanel.getSession().stop();
         if (firefoxPanel != null)
diff --git a/start.sh b/start.sh
new file mode 100755 (executable)
index 0000000..e3591c2
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+# Start the Sixth workspace with the Fallout 4 environment.
+#
+# Rebuilds every module in dependency order first, so the classpath is
+# always at the latest source state (the FO4 provider resolves `sixth`
+# and `sixth-3d` from the local Maven repository — stale installs there
+# are the classic reason the world silently doesn't show up).
+#
+# Extra args are forwarded to the JVM launcher, e.g.:
+#   ./start.sh -Dsixth.fo4.cell=ConcordExt
+# Actually: args go to Main. To pass -D properties, edit JAVA_OPTS below
+# or export them before calling.
+
+set -euo pipefail
+
+ROOT=/home/n0/workspace/svjatoslav
+FO4="$ROOT/sixth-environment-fo4"
+
+echo "==> building sixth-3d"
+mvn -f "$ROOT/sixth-3d/pom.xml" install -q -DskipTests -Dmaven.javadoc.skip=true
+
+echo "==> building sixth"
+mvn -f "$ROOT/sixth/pom.xml" install -q -DskipTests -Dmaven.javadoc.skip=true
+
+echo "==> building sixth-environment-fo4"
+mvn -f "$FO4/pom.xml" package -q -DskipTests -Dmaven.javadoc.skip=true
+mvn -f "$FO4/pom.xml" dependency:build-classpath -q -Dmdep.outputFile=target/cp.txt
+
+echo "==> starting Sixth with --env=fallout4"
+cd "$FO4"
+exec java -Xmx8g --enable-native-access=ALL-UNNAMED \
+  ${JAVA_OPTS:-} \
+  -cp "target/classes:$(cat target/cp.txt)" \
+  eu.svjatoslav.sixth.core.Main --env=fallout4 "$@"