Last active
August 16, 2023 18:20
-
-
Save Specnr/c851a92a258dd1fdbe3eee588f3f14d8 to your computer and use it in GitHub Desktop.
A multi-instance macro with instance freezing for Minecraft RSG (Depreciated, use the wall macro it has a multi mode :])
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; Multi instance AHK resetting script | |
; Author Specnr | |
; Follow setup video (mainly for OBS setup) https://youtu.be/0gaG-P2XxrE | |
; Run the setup script https://gist.github.com/Specnr/b13dae781f1b70bb6592027205870c7e | |
; Follow extra instructions https://gist.github.com/Specnr/c851a92a258dd1fdbe3eee588f3f14d8#gistcomment-3810003 | |
#NoEnv | |
#SingleInstance Force | |
SetKeyDelay, 0 | |
SetWinDelay, 1 | |
SetTitleMatchMode, 2 | |
; Variables to configure | |
global instanceFreezing := True | |
global unpauseOnSwitch := True | |
global fullscreen := False | |
global disableTTS := False | |
global countAttempts := True | |
global autoReset := False ; Resets idle worlds after 5 minutes | |
global beforeFreezeDelay := 170 ; increase if doesnt join world | |
global fullScreenDelay := 270 ; increse if fullscreening issues | |
global restartDelay := 200 ; increase if saying missing instanceNumber in .minecraft (and you ran setup) | |
global maxLoops := 20 ; increase if macro regularly locks | |
global obsDelay := 50 ; increase if not switching scenes | |
; Don't configure these | |
global currInst := -1 | |
global pauseAuto := False | |
global SavesDirectories := [] | |
global instances := 0 | |
global rawPIDs := [] | |
global PIDs := [] | |
global titles := [] | |
global resetStates := [] | |
global resetTimes := [] | |
if (instanceFreezing) { | |
UnsuspendAll() | |
sleep, %restartDelay% | |
} | |
GetAllPIDs() | |
SetTitles() | |
tmptitle := "" | |
for i, tmppid in PIDs{ | |
WinGetTitle, tmptitle, ahk_pid %tmppid% | |
titles.Push(tmptitle) | |
resetStates.push(0) | |
resetTimes.push(0) | |
WinSet, AlwaysOnTop, Off, ahk_pid %tmppid% | |
} | |
if (!disableTTS) | |
ComObjCreate("SAPI.SpVoice").Speak("Ready") | |
#Persistent | |
SetTimer, Repeat, 20 | |
return | |
Repeat: | |
Critical | |
for i, pid in PIDs { | |
HandleResetState(pid, i) | |
WinGetTitle, title, ahk_pid %pid% | |
if (title <> titles[i]) { | |
titles[i] := title | |
if (currInst > 0 && IsInGame(title)) { | |
if (i != currInst) { | |
IfWinNotActive, title | |
{ | |
while (True) { | |
if (HasGameSaved(i) || A_Index > maxLoops) | |
break | |
} | |
ControlSend, ahk_parent, {Blind}{F3 Down}{Esc}{F3 Up}, ahk_pid %pid% | |
if (instanceFreezing) { | |
sleep, %beforeFreezeDelay% | |
SuspendInstance(pid) | |
} | |
resetTimes[i] := A_TickCount | |
} | |
} | |
} | |
} | |
if (autoReset && !pauseAuto) { | |
timeDelta := 300000 ; 5 minutes | |
if (IsInGame(title) && resetStates[i] == 0 && currInst != i && resetTimes[i] > 0) { | |
if ((A_TickCount - resetTimes[i]) >= timeDelta) | |
ResetInstance(i) | |
} | |
} | |
} | |
return | |
HandleResetState(pid, idx) { | |
if (resetStates[idx] == 0) ; Not resetting | |
return | |
if (resetStates[idx] == 1 && instanceFreezing) ; Need to resume | |
ResumeInstance(pid) | |
else if (resetStates[idx] == 2) ; Exit world | |
ControlSend, ahk_parent, {Blind}{Shift down}{Tab}{Shift up}{Enter}, ahk_pid %pid% | |
else if (resetStates[idx] == 3) { ; Count attempts | |
if (countAttempts) | |
{ | |
FileRead, WorldNumber, RSG_1_16.txt | |
if (ErrorLevel) | |
WorldNumber = 0 | |
else | |
FileDelete, RSG_1_16.txt | |
WorldNumber += 1 | |
FileAppend, %WorldNumber%, RSG_1_16.txt | |
} | |
resetStates[i] := False | |
} else { ; Done | |
resetStates[idx] := -1 | |
} | |
resetStates[idx] += 1 ; Progress State | |
} | |
HasGameSaved(idx) { | |
logFile := SavesDirectories[idx] . "logs\latest.log" | |
numLines := 0 | |
Loop, Read, %logFile% | |
{ | |
numLines += 1 | |
} | |
saved := False | |
startTime := A_TickCount | |
Loop, Read, %logFile% | |
{ | |
if ((numLines - A_Index) < 5) | |
{ | |
if (InStr(A_LoopReadLine, "Loaded 0") || (InStr(A_LoopReadLine, "Saving chunks for level 'ServerLevel") && InStr(A_LoopReadLine, "minecraft:the_end"))) { | |
saved := True | |
break | |
} | |
} | |
} | |
return saved | |
} | |
RunHide(Command) | |
{ | |
dhw := A_DetectHiddenWindows | |
DetectHiddenWindows, On | |
Run, %ComSpec%,, Hide, cPid | |
WinWait, ahk_pid %cPid% | |
DetectHiddenWindows, %dhw% | |
DllCall("AttachConsole", "uint", cPid) | |
Shell := ComObjCreate("WScript.Shell") | |
Exec := Shell.Exec(Command) | |
Result := Exec.StdOut.ReadAll() | |
DllCall("FreeConsole") | |
Process, Close, %cPid% | |
Return Result | |
} | |
GetSavesDir(pid) | |
{ | |
command := Format("powershell.exe $x = Get-WmiObject Win32_Process -Filter \""ProcessId = {1}\""; $x.CommandLine", pid) | |
rawOut := RunHide(command) | |
if (InStr(rawOut, "--gameDir")) { | |
strStart := RegExMatch(rawOut, "P)--gameDir (?:""(.+?)""|([^\s]+))", strLen, 1) | |
return SubStr(rawOut, strStart+10, strLen-10) . "\" | |
} else { | |
strStart := RegExMatch(rawOut, "P)(?:-Djava\.library\.path=(.+?) )|(?:\""-Djava\.library.path=(.+?)\"")", strLen, 1) | |
if (SubStr(rawOut, strStart+20, 1) == "=") { | |
strLen -= 1 | |
strStart += 1 | |
} | |
return StrReplace(SubStr(rawOut, strStart+20, strLen-28) . ".minecraft\", "/", "\") | |
} | |
} | |
GetInstanceTotal() { | |
idx := 1 | |
global rawPIDs | |
WinGet, all, list | |
Loop, %all% | |
{ | |
WinGet, pid, PID, % "ahk_id " all%A_Index% | |
WinGetTitle, title, ahk_pid %pid% | |
if (InStr(title, "Minecraft*")) { | |
rawPIDs[idx] := pid | |
idx += 1 | |
} | |
} | |
return rawPIDs.MaxIndex() | |
} | |
GetInstanceNumberFromSaves(saves) { | |
numFile := saves . "instanceNumber.txt" | |
num := -1 | |
if (saves == "" || saves == ".minecraft" || saves == ".minecraft\" || saves == ".minecraft/") ; Misread something | |
Reload | |
if (!FileExist(numFile)) | |
MsgBox, Missing instanceNumber.txt in %saves%. Run the setup script (see instructions) | |
else | |
FileRead, num, %numFile% | |
return num | |
} | |
GetAllPIDs() | |
{ | |
global SavesDirectories | |
global PIDs | |
global instances := GetInstanceTotal() | |
; Generate saves and order PIDs | |
Loop, %instances% { | |
saves := GetSavesDir(rawPIDs[A_Index]) | |
if (num := GetInstanceNumberFromSaves(saves)) == -1 | |
ExitApp | |
PIDS[num] := rawPIDs[A_Index] | |
SavesDirectories[num] := saves | |
} | |
} | |
FreeMemory(pid) | |
{ | |
h:=DllCall("OpenProcess", "UInt", 0x001F0FFF, "Int", 0, "Int", pid) | |
DllCall("SetProcessWorkingSetSize", "UInt", h, "Int", -1, "Int", -1) | |
DllCall("CloseHandle", "Int", h) | |
} | |
UnsuspendAll() { | |
currInst := -1 | |
WinGet, all, list | |
Loop, %all% | |
{ | |
WinGet, pid, PID, % "ahk_id " all%A_Index% | |
WinGetTitle, title, ahk_pid %pid% | |
if (InStr(title, "Minecraft*")) | |
ResumeInstance(pid) | |
} | |
} | |
SuspendInstance(pid) { | |
hProcess := DllCall("OpenProcess", "UInt", 0x1F0FFF, "Int", 0, "Int", pid) | |
If (hProcess) { | |
DllCall("ntdll.dll\NtSuspendProcess", "Int", hProcess) | |
DllCall("CloseHandle", "Int", hProcess) | |
} | |
FreeMemory(pid) | |
} | |
ResumeInstance(pid) { | |
hProcess := DllCall("OpenProcess", "UInt", 0x1F0FFF, "Int", 0, "Int", pid) | |
If (hProcess) { | |
DllCall("ntdll.dll\NtResumeProcess", "Int", hProcess) | |
DllCall("CloseHandle", "Int", hProcess) | |
} | |
} | |
IsProcessSuspended(pid) { | |
WinGetTitle, title, ahk_pid %pid% | |
return InStr(title, "Not Responding") | |
} | |
SwitchInstance(idx) | |
{ | |
currInst := idx | |
pid := PIDs[idx] | |
if (instanceFreezing) | |
ResumeInstance(pid) | |
WinSet, AlwaysOnTop, On, ahk_pid %pid% | |
WinSet, AlwaysOnTop, Off, ahk_pid %pid% | |
send {Numpad%idx% down} | |
ControlSend,, {Numpad%idx%}, ahk_exe obs64.exe | |
sleep, %obsDelay% | |
send {Numpad%idx% up} | |
if (fullscreen) { | |
ControlSend, ahk_parent, {Blind}{F11}, ahk_pid %pid% | |
sleep, %fullScreenDelay% | |
} | |
Send, {LButton} ; Make sure the window is activated | |
} | |
GetActiveInstanceNum() { | |
WinGet, pid, PID, A | |
WinGetTitle, title, ahk_pid %pid% | |
if (IsInGame(title)) { | |
for i, tmppid in PIDs { | |
if (tmppid == pid) | |
return i | |
} | |
} | |
return -1 | |
} | |
IsInGame(currTitle) { ; If using another language, change Singleplayer and Multiplayer to match game title | |
return InStr(currTitle, "Singleplayer") || InStr(currTitle, "Multiplayer") || InStr(currTitle, "Instance") | |
} | |
ExitWorld() | |
{ | |
if (fullscreen) { | |
send, {F11} | |
sleep, %fullScreenDelay% | |
} | |
if (idx := GetActiveInstanceNum()) > 0 | |
{ | |
WinSet, AlwaysOnTop, Off, ahk_pid %pid% | |
nextIdx := Mod(idx, instances) + 1 | |
nextPID := PIDs[nextIdx] | |
pauseAuto := True | |
SwitchInstance(nextIdx) | |
if (unpauseOnSwitch) | |
ControlSend, ahk_parent, {Blind}{Esc}, ahk_pid %nextPID% | |
ResetInstance(idx, False) | |
pauseAuto := False | |
} | |
} | |
ResetInstance(idx, bg := True) { | |
if (bg) { | |
pid := PIDs[idx] | |
WinGetTitle, title, ahk_pid %pid% | |
if (GetActiveInstanceNum() == idx || !IsInGame(title)) | |
return | |
ControlSend, ahk_parent, {Blind}{Esc 2}, ahk_pid %pid% | |
if (instanceFreezing) | |
resetStates[idx] := 1 ; Set to Resume Instance | |
else | |
resetStates[idx] := 2 ; Set to Exit world | |
} else { | |
pid := PIDs[idx] | |
ControlSend, ahk_parent, {Blind}{Esc}, ahk_pid %pid% | |
resetStates[idx] := 2 ; Set to Exit world | |
} | |
} | |
SetTitles() { | |
for i, pid in PIDs { | |
WinSetTitle, ahk_pid %pid%, , Minecraft* - Instance %i% | |
} | |
} | |
RAlt::Suspend ; Pause all macros | |
^LAlt:: ; Reload if macro locks up | |
Reload | |
return | |
#IfWinActive, Minecraft | |
{ | |
*U:: ExitWorld() ; Reset | |
*O:: ; BG Reset Instance 1 | |
ResetInstance(1) | |
return | |
*P:: ; BG Reset Instance 2 | |
ResetInstance(2) | |
return | |
*[:: ; BG Reset Instance 3 | |
ResetInstance(3) | |
return | |
; Follow the pattern if you have more instances | |
; Remove keys you dont use to avoid complications | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Changelog
v0.24.4:
v0.24.3:
v0.24.2:
v0.24.1:
v0.24:
v0.23:
v0.22.6:
v0.22.5:
v0.22.4:
v0.22.3:
v0.22.2:
v0.22.1:
v0.22:
v0.21.1:
v0.21:
v0.20.1:
v0.20:
v0.19.8:
v0.19.7:
v0.19.6:
v0.19.5:
v0.19.4:
v0.19.3:
v0.19.2:
v0.19.1:
v0.19:
v0.18.1:
v0.18:
v0.17.4:
v0.17.3:
v0.17.2:
v0.17.1:
v0.17:
v0.16:
v0.15:
v0.14:
v0.13.1:
v0.13:
v0.12:
v0.11:
v0.10: