BareGit

Fix NetHack runtime asset paths

Compile SYSCF_FILE as a relative path so workers can open their per-game
sysconf link. Symlink immutable runtime assets into each game directory while
keeping writable state private.
Author: MetroWind <chris.corsair@gmail.com>
Date: Thu Sep 24 19:29:27 2026 -0700
Commit: 5ce9faa53e5110a08d1fba021ff11f852841dcf2

Changes

diff --git a/cmake/build_nethack.cmake b/cmake/build_nethack.cmake
index fe73a28..a308acc 100644
--- a/cmake/build_nethack.cmake
+++ b/cmake/build_nethack.cmake
@@ -48,6 +48,22 @@ if(NOT setup_result EQUAL 0)
         "${setup_output}\n${setup_error}")
 endif()
 
+# NetHack's Linux hint embeds HACKDIR in SYSCF_FILE. MCP workers run from
+# private game directories and link sysconf there, so keep this path relative.
+set(NETHACK_MAKEFILE "${NETHACK_SOURCE_DIR}/Makefile")
+file(READ "${NETHACK_MAKEFILE}" NETHACK_MAKEFILE_CONTENT)
+string(FIND "${NETHACK_MAKEFILE_CONTENT}"
+    "$(HACKDIR)/sysconf" NETHACK_SYSCF_PATH_POSITION)
+if(NETHACK_SYSCF_PATH_POSITION EQUAL -1)
+    message(FATAL_ERROR "Could not find NetHack's compiled SYSCF_FILE path")
+endif()
+string(REPLACE
+    "$(HACKDIR)/sysconf"
+    "sysconf"
+    NETHACK_MAKEFILE_CONTENT
+    "${NETHACK_MAKEFILE_CONTENT}")
+file(WRITE "${NETHACK_MAKEFILE}" "${NETHACK_MAKEFILE_CONTENT}")
+
 # Publish NetHack's native end condition before the engine tears itself down.
 set(NETHACK_END_SOURCE "${NETHACK_SOURCE_DIR}/src/end.c")
 file(READ "${NETHACK_END_SOURCE}" NETHACK_END_CONTENT)
diff --git a/include/game_session.hpp b/include/game_session.hpp
index 03c0dbc..28664a6 100644
--- a/include/game_session.hpp
+++ b/include/game_session.hpp
@@ -100,8 +100,8 @@ private:
     ToolResult refreshAgentActivity(std::int64_t& activity_at_s);
     bool persistAgentActivity(std::int64_t activity_at_s);
     void handleWorkerMessage(const Json& message);
-    bool copyRuntimeFiles(const std::filesystem::path& run_directory,
-                          std::string& error) const;
+    bool prepareRunDirectory(const std::filesystem::path& run_directory,
+                             std::string& error) const;
     static bool validGameId(const Json& arguments,
                             const Json& state,
                             std::string& error);
diff --git a/src/game_session.cpp b/src/game_session.cpp
index 14b6a5c..3fb2353 100644
--- a/src/game_session.cpp
+++ b/src/game_session.cpp
@@ -178,7 +178,7 @@ ToolResult GameSession::startGame(const Json& arguments)
 
     const std::filesystem::path run_directory = data_root_ / game_id_;
     std::string error;
-    if(!copyRuntimeFiles(run_directory, error))
+    if(!prepareRunDirectory(run_directory, error))
     {
         return errorResult("ENGINE_FAILURE", error);
     }
@@ -1087,7 +1087,7 @@ void GameSession::handleWorkerMessage(const Json& message)
     }
 }
 
-bool GameSession::copyRuntimeFiles(
+bool GameSession::prepareRunDirectory(
     const std::filesystem::path& run_directory, std::string& error) const
 {
     try
@@ -1102,6 +1102,7 @@ bool GameSession::copyRuntimeFiles(
         std::filesystem::permissions(run_directory / "save",
                                      owner_permissions,
                                      std::filesystem::perm_options::replace);
+        // Link shared immutable data into the private working directory.
         for(const char* filename : {"nhdat", "symbols", "license", "sysconf"})
         {
             const auto source = runtime_dir_ / filename;
@@ -1110,7 +1111,9 @@ bool GameSession::copyRuntimeFiles(
                 error = "NetHack runtime file is missing: " + source.string();
                 return false;
             }
-            std::filesystem::copy_file(source, run_directory / filename);
+            std::filesystem::create_symlink(
+                std::filesystem::canonical(source),
+                run_directory / filename);
         }
         for(const char* filename : {"perm", "record", "logfile",
                                     "xlogfile", "livelog"})