Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Simula.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=2]

[ext_resource path="res://addons/godot-haskell-plugin/Simula.gdns" type="Script" id=1]

[node name="Root" type="Node"]
script = ExtResource( 1 )
2 changes: 1 addition & 1 deletion addons/godot-haskell-plugin/src/Plugin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Plugin (registerClasses) where

import Godot.Nativescript

import Plugin.Simula
import Simula
import Plugin.SimulaController
import Plugin.SimulaServer
import Plugin.SimulaViewSprite
Expand Down
10 changes: 10 additions & 0 deletions addons/godot-haskell-plugin/src/Plugin/SimulaController.hs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import Foreign.C

import Godot.Api.Auto

-- | The GodotSimulaController class is responsible for handling controller input.
-- It is responsible for tracking the controller's position and orientation,
-- handling button presses, and managing the telekinesis system.
data GodotSimulaController = GodotSimulaController
{ _gscObj :: GodotObject
, _gscRayCast :: GodotRayCast
Expand Down Expand Up @@ -106,7 +109,9 @@ instance NativeScript GodotSimulaController where

-- classExtends = "ARVRController"
classMethods =
-- The 'process' function is called every frame.
[ func NoRPC "_process" (catchGodot Plugin.SimulaController.process)
-- The 'physicsProcess' function is called every physics frame.
, func NoRPC "_physics_process" (catchGodot Plugin.SimulaController.physicsProcess)
]
classSignals = []
Expand Down Expand Up @@ -239,6 +244,9 @@ addSimulaController originNode nodeName ctID = do

return ct

-- | The process function is called every frame.
-- It is responsible for updating the controller's state, such as the touchpad
-- state and the telekinesis system.
process :: GodotSimulaController -> [GodotVariant] -> IO ()
process self [deltaGV] = do
delta <- fromGodotVariant deltaGV :: IO Float
Expand Down Expand Up @@ -286,6 +294,8 @@ getWlrSeatFromPath self = do

return wlrSeat

-- | The physicsProcess function is called every physics frame.
-- It is responsible for handling the telekinesis system.
physicsProcess :: GodotSimulaController -> [GodotVariant] -> IO ()
physicsProcess self _ = do
whenM (G.get_is_active self) $ do
Expand Down
13 changes: 13 additions & 0 deletions addons/godot-haskell-plugin/src/Plugin/SimulaServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

-- | This module defines the Simula server, which is responsible for handling
-- | Wayland clients, managing windows (surfaces), and processing user input
-- | from keyboard shortcuts. It acts as the bridge between the Godot front-end
-- | and the underlying Wayland compositor logic.
module Plugin.SimulaServer where

import Data.Char
Expand Down Expand Up @@ -91,6 +95,9 @@ import qualified Data.Vector as V

import qualified Data.Text as T

-- | Translates a 'KeyboardShortcut' from the configuration file into a
-- | corresponding 'KeyboardAction' (a function that can be executed).
-- | This acts as a central dispatcher for all keyboard commands.
getKeyboardAction :: GodotSimulaServer -> KeyboardShortcut -> KeyboardAction
getKeyboardAction gss keyboardShortcut =
case (keyboardShortcut ^. keyAction) of
Expand Down Expand Up @@ -254,6 +261,9 @@ getKeyboardAction gss keyboardShortcut =
rotateWorkspacesHorizontally gss radians _ False = do
return ()

-- | Toggles screen recording using ffmpeg. On the first press, it starts
-- | recording the screen to a timestamped file in SIMULA_DATA_DIR.
-- | On the second press, it stops the recording.
recordScreen :: GodotSimulaServer -> SpriteLocation -> Bool -> IO ()
recordScreen gss _ True = do
maybePh <- readTVarIO (gss ^. gssScreenRecorder)
Expand Down Expand Up @@ -283,6 +293,8 @@ getKeyboardAction gss keyboardShortcut =
recordScreen gss _ False = do
return ()

-- | Sends a "close" request to the Wayland surface currently under the cursor.
-- | This is a graceful termination request, not a forceful kill.
terminateWindow :: SpriteLocation -> Bool -> IO ()
terminateWindow (Just (gsvs, coords@(SurfaceLocalCoordinates (sx, sy)))) True = do
simulaView <- readTVarIO (gsvs ^. gsvsView)
Expand All @@ -297,6 +309,7 @@ getKeyboardAction gss keyboardShortcut =
G.send_close wlrXWaylandSurface
terminateWindow _ _ = return ()

-- | Launches an arbitrary shell command. Used for applications like terminals.
shellLaunch :: GodotSimulaServer -> String -> SpriteLocation -> Bool -> IO ()
shellLaunch gss shellCmd _ True = do
appLaunch gss shellCmd Nothing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}

module Plugin.Simula (GodotSimula(..)) where
module Simula (GodotSimula(..)) where

import Plugin.Imports
import Data.Maybe
Expand Down Expand Up @@ -36,6 +36,9 @@ import Godot.Gdnative.Internal ( GodotNodePath

import System.Environment

-- | The GodotSimula class is the main entry point for the Simula VR window manager.
-- It is responsible for initializing the VR environment, handling user input,
-- and managing the state of the application.
data GodotSimula = GodotSimula
{ _sObj :: GodotObject
, _sGrabState :: TVar GrabState
Expand All @@ -48,9 +51,12 @@ instance NativeScript GodotSimula where

-- classExtends = "Node"
classMethods =
[ func NoRPC "_ready" (catchGodot Plugin.Simula.ready)
, func NoRPC "_process" (catchGodot Plugin.Simula.process)
, func NoRPC "on_button_signal" (catchGodot Plugin.Simula.on_button_signal)
-- The 'ready' function is called when the node enters the scene tree.
[ func NoRPC "_ready" (catchGodot ready)
-- The 'process' function is called every frame.
, func NoRPC "_process" (catchGodot process)
-- The 'on_button_signal' function is called when a controller button is pressed.
, func NoRPC "on_button_signal" (catchGodot on_button_signal)
]
classSignals = []

Expand All @@ -59,6 +65,9 @@ instance HasBaseClass GodotSimula where
super (GodotSimula obj _) = GodotNode obj


-- | The ready function is called when the node enters the scene tree.
-- It is responsible for initializing the VR environment, setting up the controllers,
-- and loading the pancake camera.
ready :: GodotSimula -> [GodotVariant] -> IO ()
ready self _ = do
-- OpenHMD is unfortunately not yet a working substitute for OpenVR
Expand Down Expand Up @@ -183,6 +192,9 @@ ready self _ = do
return ()


-- | The on_button_signal function is called when a controller button is pressed.
-- It is responsible for handling button events and dispatching them to the
-- appropriate handler.
on_button_signal :: GodotSimula -> [GodotVariant] -> IO ()
on_button_signal self [buttonVar, controllerVar, pressedVar] = do
-- putStrLn "on_button_signal in Simula.hs"
Expand Down Expand Up @@ -228,6 +240,9 @@ onButton self gsc button pressed = do
_ -> const $ return ()


-- | The process function is called every frame.
-- It is responsible for handling the state of the application, such as
-- processing grab events.
process :: GodotSimula -> [GodotVariant] -> IO ()
process self _ = do
-- putStrLn "process in Simula.hs"
Expand Down
174 changes: 24 additions & 150 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -116,26 +116,7 @@

monado = inputs.monado.packages.${system}.default;

simulaMonadoServiceContent = ''
#!${pkgs.stdenv.shell}
${xdgAndSimulaEnvVars}
pkill monado-service
export SIMULA_CONFIG_PATH=$SIMULA_NIX_DIR/opt/simula/config/simula_monado_config.json
export XR_RUNTIME_JSON=$SIMULA_NIX_DIR/opt/simula/config/active_runtime.json
export XRT_COMPOSITOR_LOG=debug
export XRT_COMPOSITOR_SCALE_PERCENTAGE=100
# If --local is passed, use the monado binary compiled in ./submodules/monado
if [[ "''${1:-}" == "--local" ]]; then
shift # remove --local so $@ now contains only user args
MONADO_BINARY="./submodules/monado/build/src/xrt/targets/service/monado-service"
else
MONADO_BINARY="${monado}/bin/monado-service"
fi
$MONADO_BINARY 2>&1 | tee "$SIMULA_LOG_DIR/monado.log"
'';


cleanSourceFilter =
name: type:
Expand Down Expand Up @@ -171,135 +152,21 @@
chmod 755 $out/opt/simula/addons/godot-haskell-plugin/bin/x11/libgodot-haskell-plugin.so
'';

initiateSimulaRunner = ''
#!/usr/bin/env sh

# Exit `simula` launcher early if anything weird happens
set -o errexit
set -o nounset
set -o pipefail
'';

monadoEnvVars = ''
export SIMULA_CONFIG_PATH=./config/simula_monado_config.json
export XR_RUNTIME_JSON=./config/active_runtime.json
export XRT_COMPOSITOR_LOG=debug
export XRT_COMPOSITOR_SCALE_PERCENTAGE=100
'';

xdgAndSimulaEnvVars = ''
export XDG_CACHE_HOME=''${XDG_CACHE_HOME:-$HOME/.cache}
export XDG_DATA_HOME=''${XDG_DATA_HOME:-$HOME/.local/share}
export XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
export SIMULA_NIX_DIR="$(dirname "$(dirname "$(readlink -f "$0")")")"
export SIMULA_LOG_DIR="$XDG_CACHE_HOME/Simula"
export SIMULA_DATA_DIR="$XDG_DATA_HOME/Simula"
export SIMULA_CONFIG_DIR="$XDG_CONFIG_HOME/Simula"
echo "XDG_CACHE_HOME: $XDG_CACHE_HOME"
echo "XDG_DATA_HOME: $XDG_DATA_HOME"
echo "XDG_CONFIG_HOME: $XDG_CONFIG_HOME"
echo "SIMULA_NIX_DIR: $SIMULA_NIX_DIR"
echo "SIMULA_LOG_DIR: $SIMULA_LOG_DIR"
echo "SIMULA_DATA_DIR: $SIMULA_DATA_DIR"
echo "SIMULA_CONFIG_DIR: $SIMULA_CONFIG_DIR"
'';



# Needed to ensure fonts don't show up as blocks on certain non-NixOS distributions, IIRC
fontEnvVars = ''
export LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive
'';

copySimulaConfigFiles = ''
# Copy over default config files if they don't already exist
if [ ! -f "$SIMULA_CONFIG_DIR/HUD.config" ]; then
mkdir -p "$SIMULA_CONFIG_DIR"
cp "$SIMULA_NIX_DIR/opt/simula/config/HUD.config" "$SIMULA_CONFIG_DIR/HUD.config"
fi
if [ ! -f "$SIMULA_CONFIG_DIR/config.dhall" ]; then
mkdir -p "$SIMULA_CONFIG_DIR"
cp "$SIMULA_NIX_DIR/opt/simula/config/config.dhall" "$SIMULA_CONFIG_DIR/config.dhall"
fi
if [ ! -d "$SIMULA_DATA_DIR/environments" ]; then
mkdir -p "$SIMULA_DATA_DIR"
cp -R "$SIMULA_NIX_DIR/opt/simula/environments" "$SIMULA_DATA_DIR/environments"
fi
'';

launchSimula = ''
# Use --local if you want to launch Simula locally (with godot binary from ./submodules/godot)
if [ "''${1:-}" = "--local" ]; then
GODOT_BINARY="./submodules/godot/bin/godot.x11.tools.64"
PROJECT_PATH="./."
shift # remove --local so $@ now contains only user args
# Otherwise, use the nix store for everything
else
GODOT_BINARY="${inputs.godot.packages."${system}".godot}/bin/godot"
PROJECT_PATH="$SIMULA_NIX_DIR/opt/simula"
# Ensure that we're in the right directory before launching so the relative res:// paths work correctly
# (I tried using absolute paths instead of res://, but godot doesn't seem to play well so we use this hack)
cd "$PROJECT_PATH"
PROJECT_PATH="./."
fi
# We `script` (+ stdbuf and ansi2text) to tee the output into the console (colorized) and into our log files (non-colorized)
if grep -qi NixOS /etc/os-release; then
${pkgs.util-linux}/bin/script -qfc "$GODOT_BINARY -m \"$PROJECT_PATH\" $@" >(${pkgs.coreutils}/bin/stdbuf -oL -eL ${pkgs.colorized-logs}/bin/ansi2txt > "$SIMULA_DATA_DIR/log/output.file")
else
echo "Detected non-NixOS distribution, so running Simula with nixGL"
nix run --impure github:nix-community/nixGL -- ${pkgs.util-linux}/bin/script -qfc "$GODOT_BINARY -m \"$PROJECT_PATH\" $@" >(${pkgs.coreutils}/bin/stdbuf -oL -eL ${pkgs.colorized-logs}/bin/ansi2txt > "$SIMULA_DATA_DIR/log/output.file")
fi
'';


simula = pkgs.stdenv.mkDerivation rec {
pname = "simula";
version = "0.0.0";

src = lib.cleanSourceWith {
filter = cleanSourceFilter;
src = ./.;
};

nativeBuildInputs = [
pkgs.autoPatchelfHook
pkgs.makeWrapper
];

buildInputs = [
pkgs.systemd
pkgs.openxr-loader
];

dontBuild = true;

# Force certain nix programs that Simula needs at runtime to be in the front of the user's PATH
# (We use this strategy instead of just adding the programs to bin/* to avoid
# potential conflicts if the user has already installed them via nix)
passthru.simulaRuntimePrograms = with pkgs; [
xpra
xorg.xrdb
wmctrl
ffmpeg
synapse
xsel
mimic
xclip
xfce4-terminal-wrapped
midori-wrapped
i3status-wrapped
xorg.xkbcomp
xwayland
];

passthru.simulaRuntimeLibs = with pkgs; [
openxr-loader
libv4l
];

installPhase = ''
runHook preInstall
Expand All @@ -308,24 +175,31 @@
${placeGodotHaskellPluginLib}
mkdir -p $out/bin
cat > $out/bin/simula-unwrapped << 'EOF'
${initiateSimulaRunner}
${monadoEnvVars}
${xdgAndSimulaEnvVars}
${fontEnvVars}
${copySimulaConfigFiles}
${launchSimula} "$@"
simulaUnwrapped=${pkgs.substituteAll {
src = ./nix/simula-unwrapped.sh;
inherit (pkgs) util-linux coreutils colorized-logs;
glibcLocales = pkgs.glibcLocales;
godot = inputs.godot.packages."${system}".godot;
}}
cat > $out/bin/simula-unwrapped << EOF
''${simulaUnwrapped}
EOF
chmod 755 $out/bin/simula-unwrapped
# A wrapped `simula` includes various helper programs that Simula
# needs guarantees are available at runtime.
makeWrapper $out/bin/simula-unwrapped $out/bin/simula \
--prefix PATH : ${lib.makeBinPath passthru.simulaRuntimePrograms} \
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath passthru.simulaRuntimeLibs}
--prefix PATH : ${lib.makeBinPath passthru.simulaRuntimePrograms} \
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath passthru.simulaRuntimeLibs}
Comment on lines +192 to +193
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ nix flake check -L
error: undefined variable 'passthru'
       at /home/haruki/program-dir/Simula/flake.nix:192:51:
          191|               makeWrapper $out/bin/simula-unwrapped $out/bin/simula \
          192|                 --prefix PATH : ${lib.makeBinPath passthru.simulaRuntimePrograms} \
             |                                                   ^
          193|                 --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath passthru.simulaRuntimeLibs}

Probably you can use nix flake check command. Please try it~

My environment is:

$ nix-shell -p nix-info --run "nix-info -m"
 - system: `"x86_64-linux"`
 - host os: `Linux 6.12.53, NixOS, 25.11 (Xantusia), 25.11.20251018.3622652`
 - multi-user?: `yes`
 - sandbox: `yes`
 - version: `nix-env (Nix) 2.31.2`
 - nixpkgs: `/nix/store/3qb1vcqah87n5hi5pd57m7jmjn78lpkb-source`

simulaMonadoService=${pkgs.substituteAll {
src = ./nix/simula-monado-service.sh;
inherit (pkgs) stdenv;
inherit monado;
}}
cat > $out/bin/simula-monado-service << 'EOF'
${simulaMonadoServiceContent}
cat > $out/bin/simula-monado-service << EOF
''${simulaMonadoService}
EOF
chmod 755 $out/bin/simula-monado-service
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ build-monado:
nix develop ./submodules/monado?submodules=1#default --command bash -c "cd ./submodules/monado && just build"

build-monado-watch:
nix develop ./submodules/monado?submodules=1#default --command bash -c "cd ./submodules/monado && just build"
nix develop ./submodules/monado?submodules=1#default --command bash -c "cd ./submodules/monado && just build-watch"

run-monado:
./result/bin/simula-monado-service --local
Expand Down
Loading