Skip to content

Instantly share code, notes, and snippets.

@Specnr
Last active August 16, 2023 18:20
Show Gist options
  • Save Specnr/c851a92a258dd1fdbe3eee588f3f14d8 to your computer and use it in GitHub Desktop.
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 :])
; 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
}
@Specnr
Copy link
Author

Specnr commented Jul 17, 2021

Changelog

v0.24.4:

  • Fixed OBS not scene switching issues, past fix only worked for me LULW

v0.24.3:

  • Fixed OBS hotkey issues and removed any delay associated
  • Removed world moving, use separate script instead

v0.24.2:

  • probably fixed issue where it would say missing instance number when it wasnt missing.
  • Set default autoreset feature to False to match srdc ruleset

v0.24.1:

  • Fix for bg resetting while instance freezing off

v0.24:

  • Added instance freezing as an option
  • I recommend you keep it on, but if you have a lot of RAM, try it off with the dynamic FPS mod

v0.23:

  • Removed need for switch delay :)

v0.22.6:

  • Added fix for macro locking

v0.22.5:

  • Added fix for fullscreen

v0.22.4:

  • Changed the way i unpause on switch to allow for better reliability

v0.22.3:

  • Added pause on world load to be toggleable to allow for faster resets

v0.22.2:

  • Made universally work between 1.16, 1.15 and Icarus 1.16
  • (Maybe more, only tested those 3)

v0.22.1:

  • Made work with spaces in folder structure

v0.22:

  • Added new setup script to simplify things (see above)
  • Removed need for taskbar order at all (after using setup script)
  • Allowed for world moving across drives

v0.21.1:

  • Changed back to winactivate for switching to make more consistent.
  • Taskbar order no longer matters after initial load/reload

v0.21:

  • Removed need for saves directories
  • Removed need for switch buffer allowing for much lower switch delay
  • Watch New setup video for how to use this new style of macro

v0.20.1:

  • Added line back that i missed lol

v0.20:

  • Added dependency of new livesplit dll linked above
  • To install just download the .dll file and move it into into your Livesplit folder -> Components replacing the current. Then check both boxes in LiveSplit's IGT settings
  • Speeds up resets and removes any livesplit related issues

v0.19.8:

  • Added option to use multiple livesplits to avoid some delay at the cost of convenience

v0.19.7:

  • Auto Reset after 5 minutes now works as expected

v0.19.6:

  • Made resets faster by switching before killing livesplit

v0.19.5:

  • Fixed not moving worlds

v0.19.4:

  • Added fix for non-multimc users

v0.19.3:

  • Fixed all issues with uncrouching and other issues while background resetting
  • Added instance number indicator to old worlds to make verification easier

v0.19.2:

  • Removed need for any structure to instance folder names

v0.19.1:

  • Added fix for newer versions of autoreset renaming worlds

v0.19:

  • Made work with auto reset, also now DEPENDS on it
  • Download link above

v0.18.1:

  • Added back shift calls to exit world to fix issues when CHECKING
  • WARNING: if you use shift for anything, disable autoReset for now

v0.18:

  • Single LiveSplit support with updating saves paths for IGT
  • Added F12 hotkey to set titles before opening OBS at the start of a session to allow for easy setup
  • Added steps to fix macro freezing in the comment above

v0.17.4:

  • Removed all shift calls
  • Resetting in spectator kinda weird, either don't or after pressing button go back and manually exit the world

v0.17.3:

  • Moved shift calls to one state to reduce shifting error

v0.17.2:

  • Press F3+Esc on world load for better visibility bg resets
  • You need to press F3+P on all instances

v0.17.1:

  • Made memory freeing more efficient

v0.17:

  • Added RAM freeing for suspended instances

v0.16:

  • Added feature to automatically background reset after 5 minutes

v0.15:

  • Tested and fixed some issues with fullscreen

v0.14:

  • Added reset state management to allow for more efficient resetting

v0.13.1:

  • Small fix for fullscreen users

v0.13:

  • Fixed trying to bg reset while bg resetting

v0.12:

  • Fixed issue with macro infinite loop case

v0.11:

  • Fixed issues with window switching

v0.10:

  • Added reset queue to allow for quicker resetting

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment