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
38 changes: 38 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,44 @@ tasks.prepareSandbox {
val file = file(f)
if (!file.exists()) throw RuntimeException("File ${file} does not exist")
})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tested without these changes and get

Image

So I believe they're needed.

// The Rider SDK archive omits certain DLLs that are present in a full Rider installation.
// Copy the missing Unity plugin DotFiles DLL from the local Rider installation so the sandbox can load it.
if (!isWindows) {
val riderInstallCandidates = if (Os.isFamily(Os.FAMILY_MAC)) {
listOf(file("/Applications/Rider.app/Contents"))
} else {
// Linux: check JetBrains Toolbox and common standalone install paths
val toolboxBase = file("${System.getProperty("user.home")}/.local/share/JetBrains/Toolbox/apps/Rider")
val toolboxInstalls = if (toolboxBase.exists()) {
toolboxBase.walkTopDown()
.filter { it.name == "plugins" && it.parentFile?.name?.startsWith("2") == true }
.map { it.parentFile }
.toList()
} else emptyList()
toolboxInstalls + listOf(file("/opt/rider"), file("/usr/share/rider"))
}

val missingDotFileDlls = listOf(
"JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.PausePoint.Helper.dll",
"JetBrains.ReSharper.Plugins.Unity.Rider.Debugger.Presentation.Texture.dll",
)

val destDir = intellijPlatform.platformPath.resolve("plugins/rider-unity/DotFiles").toFile()
destDir.mkdirs()

for (dllName in missingDotFileDlls) {
val dllRelPath = "plugins/rider-unity/DotFiles/$dllName"
val srcDll = riderInstallCandidates
.map { file("${it}/${dllRelPath}") }
.firstOrNull { it.exists() }

if (srcDll != null) {
// Copy into the extracted SDK location (platformPath) — that's where Rider loads plugins from at runtime
srcDll.copyTo(file("${destDir}/${srcDll.name}"), overwrite = true)
}
}
}
}
}

Expand Down
34 changes: 13 additions & 21 deletions src/rider/main/kotlin/run/RunConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import com.jetbrains.rider.plugins.unity.UnityBundle
import com.jetbrains.rider.plugins.unity.run.configurations.unityExe.UnityExeConfiguration
import com.jetbrains.rider.run.RiderRunBundle
import icons.UnityIcons
import kotlin.io.path.Path


internal class UnityPlayerDebugConfigurationTypeInternal : ConfigurationTypeBase(
ID,
Expand Down Expand Up @@ -108,7 +106,7 @@ class RunConfiguration(project: Project, factory: ConfigurationFactory, name: St
getScriptName(),
getSaveFilePath(),
getModListPath(),
getRimworldState(environment, OS.CURRENT == OS.Linux),
getRimworldState(environment),
UnityDebugRemoteConfiguration(),
environment,
"CustomPlayer"
Expand All @@ -119,30 +117,24 @@ class RunConfiguration(project: Project, factory: ConfigurationFactory, name: St
return RimworldDev.Rider.run.SettingsEditor(project)
}

private fun getRimworldState(environment: ExecutionEnvironment, debugInLinux: Boolean = false): CommandLineState {
private fun getRimworldState(environment: ExecutionEnvironment): CommandLineState {
return object : CommandLineState(environment) {
override fun startProcess(): ProcessHandler {
var pathToRun = getScriptName()
var arguments = getCommandLineOptions()

// If we're debugging in Rimworld, instead of /pwd/RimWorldLinux ...args we want to run /bin/sh /pwd/run.sh /pwd/RimWorldLinux ...args
if (debugInLinux) {
val bashScriptPath = "${Path(pathToRun).parent}/run.sh"
arguments = "$bashScriptPath $pathToRun $arguments"
pathToRun = "/bin/sh"
}

if (OS.CURRENT == OS.macOS) {
arguments = "$pathToRun $arguments"
pathToRun = "open"
val scriptName = getScriptName()
val extraArgs = getCommandLineOptions().split(' ').filter { it.isNotEmpty() }

val commandLine = when {
OS.CURRENT == OS.macOS -> {
val params = if (extraArgs.isEmpty()) listOf(scriptName)
else listOf(scriptName, "--args") + extraArgs
GeneralCommandLine("open").withParameters(params)
}
else -> GeneralCommandLine(scriptName).withParameters(extraArgs)
}

val commandLine = GeneralCommandLine(pathToRun)
.withParameters(arguments.split(' ').filter { it.isNotEmpty() })

EnvironmentVariablesData.create(getEnvData(), true).configureCommandLine(commandLine, true)

QuickStartUtils.setup(getModListPath(), getSaveFilePath());
QuickStartUtils.setup(getModListPath(), getSaveFilePath())

val processHandler = ProcessHandlerFactory.getInstance()
.createColoredProcessHandler(commandLine)
Expand Down
89 changes: 72 additions & 17 deletions src/rider/main/kotlin/run/RunState.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package RimworldDev.Rider.run

import RimworldDev.Rider.helpers.ScopeHelper
import com.intellij.execution.ExecutionException
import com.intellij.execution.ExecutionResult
import com.intellij.execution.Executor
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.process.*
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ProgramRunner
import com.intellij.ide.actions.searcheverywhere.evaluate
import com.intellij.util.system.OS
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rider.debugger.DebuggerWorkerProcessHandler
import com.jetbrains.rider.plugins.unity.run.configurations.UnityAttachProfileState
import com.jetbrains.rider.run.configurations.remote.RemoteConfiguration
import com.jetbrains.rider.run.getProcess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.net.BindException
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.nio.file.Files
import kotlin.io.path.Path

Expand Down Expand Up @@ -59,12 +65,16 @@ class RunState(
"Doorstop/pdb2mdb.exe",
),
OS.macOS to listOf(
"run.sh",
".doorstop_version",
".doorstop_config.ini",
"doorstop_config.ini",

"Doorstop/0Harmony.dll",
"Doorstop/dnlib.dll",
"Doorstop/Doorstop.dll",
"Doorstop/Doorstop.pdb",
"Doorstop/HotReload.dll",
"Doorstop/libdoorstop.dylib",
"Doorstop/Mono.Cecil.dll",
"Doorstop/Mono.CompilerServices.SymbolWriter.dll",
"Doorstop/pdb2mdb.exe",
Expand All @@ -78,31 +88,76 @@ class RunState(
lifetime: Lifetime
): ExecutionResult {
setupDoorstop()
val result = super.execute(executor, runner, workerProcessHandler)
ProcessTerminatedListener.attach(workerProcessHandler.debuggerWorkerRealHandler)

val rimworldResult = rimworldState.execute(executor, runner)
workerProcessHandler.debuggerWorkerRealHandler.addProcessListener(createProcessListener(rimworldResult?.processHandler))
QuickStartUtils.setup(modListPath, saveFilePath)

// On macOS/Linux we must bypass rimworldState.execute() for two reasons:
// 1. CommandLineState.execute() has implicit EDT dependencies (console view creation)
// that fail silently when called from this suspend function's coroutine context.
// 2. On macOS, the 'open' command launches apps through launchd, which is SIP-protected
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a comment here to explain why this is required on mac for future reference

// and strips DYLD_INSERT_LIBRARIES — preventing Doorstop injection entirely.
// ProcessBuilder with run.sh avoids both: it fork/exec's directly, preserving DYLD_* vars,
// and has no IntelliJ threading requirements. Windows uses DLL hijacking (winhttp.dll)
// instead of DYLD injection, so rimworldState.execute() works fine there.
val gameProcess: Process? = if (OS.CURRENT == OS.macOS || OS.CURRENT == OS.Linux) {
val bashScriptPath = "${Path(rimworldLocation).parent}/run.sh"
withContext(Dispatchers.IO) {
val logFile = File(System.getProperty("java.io.tmpdir"), "rimworld-doorstop.log")
ProcessBuilder("/bin/sh", bashScriptPath, rimworldLocation)
.redirectErrorStream(true)
.redirectOutput(logFile)
.start()
}
} else {
// Windows: rimworldState handles env vars and process lifecycle normally.
rimworldState.execute(executor, runner)?.processHandler?.getProcess()
}

return result
}
// Poll until the Mono debug server is reachable before attaching the debugger.
if (!waitForMonoDebugServer()) {
throw ExecutionException(
"Timed out waiting for Mono debug server on port 56000. " +
"The game process may have failed to start, or Doorstop may not have injected. " +
"Check that run.sh is executable and that libdoorstop.dylib is present in the game directory."
)
}

private fun createProcessListener(siblingProcessHandler: ProcessHandler?): ProcessListener {
return object : ProcessAdapter() {
val result = super.execute(executor, runner, workerProcessHandler)
ProcessTerminatedListener.attach(workerProcessHandler.debuggerWorkerRealHandler)
workerProcessHandler.debuggerWorkerRealHandler.addProcessListener(object : ProcessAdapter() {
override fun processTerminated(event: ProcessEvent) {
val processHandler = event.processHandler
processHandler.removeProcessListener(this)

event.processHandler.removeProcessListener(this)
if (OS.CURRENT == OS.Linux) {
siblingProcessHandler?.getProcess()?.destroyForcibly()
gameProcess?.destroyForcibly()
} else {
siblingProcessHandler?.getProcess()?.destroy()
gameProcess?.destroy()
}

QuickStartUtils.tearDown(saveFilePath)
removeDoorstep()
}
})

return result
}

private suspend fun waitForMonoDebugServer(port: Int = 56000, timeoutMs: Long = 60_000): Boolean {
val deadline = System.currentTimeMillis() + timeoutMs
while (System.currentTimeMillis() < deadline) {
// Bind to 127.0.0.1 explicitly — on macOS, 0.0.0.0 and 127.0.0.1 are distinct
// bind targets, so ServerSocket(port) alone wouldn't detect Mono listening.
val isListening = withContext(Dispatchers.IO) {
try {
ServerSocket().use { it.bind(InetSocketAddress("127.0.0.1", port)) }
false
} catch (_: BindException) {
true
} catch (_: Exception) {
false
}
}
if (isListening) return true
delay(500)
}
return false
}

private fun setupDoorstop() {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ enabled=true

# Path to the assembly to load and execute
# NOTE: The entrypoint must be of format `static void Doorstop.Entrypoint.Start()`
target_assembly=Doorstop\Doorstop.dll
target_assembly=Doorstop/Doorstop.dll

# If true, Unity's output log is redirected to <current folder>\output_log.txt
redirect_output_log=false
Expand Down
Loading