Skip to content

Instantly share code, notes, and snippets.

@sheeperino
Last active August 18, 2021 14:49
Show Gist options
  • Save sheeperino/3eebd4afe0cb35280fbe2e3ab155b72a to your computer and use it in GitHub Desktop.
Save sheeperino/3eebd4afe0cb35280fbe2e3ab155b72a to your computer and use it in GitHub Desktop.
Custom Specnr Macro with The Wall
; Multi instance AHK resetting script
; Author: Specnr, rewritten by Sheep
;Follow Four's setup tutorial: https://youtu.be/PTgS3z1O5ws
;Specnr's tutorial: https://youtu.be/0gaG-P2XxrE
;Rename all practice worlds to begin with an _ to keep them from being relocated
;Have at least one kept world in each saves directory
;Check the GitHub post for more instructions
#NoEnv
#SingleInstance Force
#WinActivateForce
#MaxThreadsPerHotkey 10
Process, Priority, , A
SetDefaultMouseSpeed, 1
SetKeyDelay, -1
SetTitleMatchMode, 2
SetBatchLines, -1
CoordMode, Mouse, Client
; Variables to configure
global autoReset := True ; Resets idle worlds after 5 minutes. Disable if using Shift
global beforeFreezeDelay := 550 ; delay before the world freezes after loading
global switchDelay := 20 ; increase if window switching lags
global fullScreenDelay := 100 ; time it takes to toggle fullscreen in ms
global obsDelay := 100 ; increase if not changing scenes in obs
global LiveSplitReset := "Numpad6" ; Reset macro for LiveSplit
global LiveSplitStart := "Numpad5" ; Start macro for LiveSplit
global WallHotkey := "Numpad4" ; Wall Scene macro for OBS
; Add/remove directories to array to account for more/less
global oldWorldsFolder := "C:\Users\Sophie\Desktop\MultiMC\instances\1.16.11\.minecraft\oldWorlds\"
global SavesDirectories = ["C:\Users\Sophie\Desktop\MultiMC\instances\1.16.11\.minecraft\saves\", "C:\Users\Sophie\Desktop\MultiMC\instances\1.16.12\.minecraft\saves\", "C:\Users\Sophie\Desktop\MultiMC\instances\1.16.13\.minecraft\saves\"]
; The Wall
global Width := 1280
global Height := 720
global SecWidth := 427 ; width and
global SecHeight := 720 ; height for a single section
global Map := [[1, 2, 3]] ; Create a new array for a new row
; Don't configure these
global index
global pauseAuto := False
global instances := SavesDirectories.MaxIndex()
global PIDs := GetAllPIDs()
global titles := []
global resetStates := []
global resetTimes := []
tmptitle := ""
for i, tmppid in PIDs{
WinGetTitle, tmptitle, ahk_pid %tmppid%
titles.Push(tmptitle)
resetStates.push(0)
resetTimes.push(0)
}
IfNotExist, %oldWorldsFolder%
FileCreateDir %oldWorldsFolder%
#Persistent
SetTimer, Repeat, 20
return
Repeat:
Critical
for i, pid in PIDs {
if (needToResetTimer) {
if (WinExist("LiveSplit")) {
send, {%LiveSplitReset%}{%LiveSplitStart%}
needToResetTimer := False
}
}
HandleResetState(pid, i)
WinGetTitle, title, ahk_pid %pid%
if (title <> titles[i]) {
titles[i] := title
IfWinNotActive, title
if (IsInGame(title)) {
while (True) {
if (HasGameSaved(i, False))
break
}
ControlSend, ahk_parent, {F3 Down}{Esc}{F3 Up}, ahk_pid %pid%
OutputDebug, paused the game
sleep, %beforeFreezeDelay%
SuspendInstance(pid)
OutputDebug, suspended instance
resetTimes[i] := A_NowUTC
}
}
if (autoReset && !pauseAuto) {
timeDelta := 500 ; 5 minutes
if (IsInGame(title) && resetStates[i] == 0 && index != i && resetTimes[i] > 0) {
if ((A_NowUTC - resetTimes[i]) >= timeDelta)
ResetInstance(i)
}
}
}
return
HandleResetState(pid, idx) {
if (resetStates[idx] == 0) ; Not resetting
return
if (resetStates[idx] == 1) ; Need to resume
ResumeInstance(pid)
else if (resetStates[idx] == 2) { ; Exit world
ControlSend, ahk_parent, {Shift down}{Tab}{Shift up}{Enter}, ahk_pid %pid%
}
else if (resetStates[idx] == 3) { ; Move worlds
MoveWorlds(idx)
resetStates[i] := False
} else { ; Done
resetStates[idx] := -1
}
resetStates[idx] += 1 ; Progress State
}
SetTitles() {
for i, pid in PIDs {
WinSetTitle, ahk_pid %pid%, , Minecraft* - Instance %i%
}
}
WallCoords()
{
MouseGetPos, mX, mY
if (mX < Width && mX > 0 && mY < Height && mY > 0) {
x := mX // SecWidth +1
y := mY // SecHeight +1
index := map[y][x]
return index
}
}
MoveWorlds(idx)
{
dir := SavesDirectories[idx]
Loop, Files, %dir%*, D
{
_Check :=SubStr(A_LoopFileName,1,1)
If (InStr(A_LoopFileName, "New World") || InStr(A_LoopFileName, "Speedrun #"))
FileMoveDir, %dir%%A_LoopFileName%, %oldWorldsFolder%%A_LoopFileName%%A_NowUTC%, R
}
}
IsInGame(currTitle) { ; If using another language, change Singleplayer and Multiplayer to match game title
return InStr(currTitle, "Singleplayer") || InStr(currTitle, "Multiplayer") || InStr(currTitle, "Instance")
}
ResetInstance(idx, Wall := True) {
if (idx){
if (Wall) {
pid := PIDs[idx]
WinGetTitle, title, ahk_pid %pid%
if (!IsInGame(title))
return
ControlSend, ahk_parent, {Esc 2}, ahk_pid %pid%
resetStates[idx] := 1 ; Set to Resume Instance
} else {
pid := PIDs[idx]
ControlSend, ahk_parent, {Esc}, ahk_pid %pid%
resetStates[idx] := 2 ; Set to Exit world
}
}
}
PlayInstance(idx) {
if (idx) {
OutputDebug, playing instance %idx%
pid := PIDs[idx]
ResumeInstance(PIDs[idx])
OutputDebug, pid %pid%
sleep, %switchDelay%
WinActivate, ahk_pid %pid%
ControlSend, ahk_parent, {Esc}, ahk_pid %pid%
send {Numpad%idx% down}
sleep, %obsDelay%
send {Numpad%idx% up}
}
}
ExitWorld() {
if (fullscreen) {
send, {F11}
sleep, %fullScreenDelay%
}
SwitchToWall()
ResetInstance(index, False)
OutputDebug, reset instance %index% with %A_ThisHotkey%
}
SwitchToWall() {
CoordMode, Mouse, Client
WinMove, Windowed Projector, , , , Width +16, Height +39
WinGetPos,,, X, Y, Windowed Projector
WinMove, Windowed Projector, , (A_ScreenWidth/2)-(X/2), (A_ScreenHeight/2)-(Y/2 +11)
Send, {%WallHotkey% down}
sleep, %obsDelay%
Send, {%WallHotkey% up}
WinActivate, Windowed Projector
}
SwitchToInstance() { ; Unused
pid := PIDs[idx]
; SwitchToBGScreen()
sleep, %switchDelay%
WinActivate, ahk_pid %pid%
send {Esc}
}
GetInstanceNum(pid)
{
command := Format("powershell.exe $x = Get-WmiObject Win32_Process -Filter \""ProcessId = {1}\""; $x.CommandLine", pid)
rawOut := RunHide(command)
for i, savesDir in SavesDirectories {
StringTrimRight, tmp, savesDir, 18
subStr := StrReplace(tmp, "\", "/")
if (InStr(rawOut, subStr))
return i
}
return -1
}
RunHide(Command)
{
OutputDebug, runhide
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
}
FreeMemory(pid)
{
h:=DllCall("OpenProcess", "UInt", 0x001F0FFF, "Int", 0, "Int", pid)
DllCall("SetProcessWorkingSetSize", "UInt", h, "Int", -1, "Int", -1)
DllCall("CloseHandle", "Int", h)
}
UnsuspendAll() {
for i, pid in PIDs {
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)
}
}
GetAllPIDs()
{
OutputDebug, getting all pids
orderedPIDs := []
loop, %instances%
orderedPIDs.Push(-1)
WinGet, all, list
Loop, %all%
{
WinGet, pid, PID, % "ahk_id " all%A_Index%
WinGetTitle, title, ahk_pid %pid%
if (InStr(title, "Minecraft* 1.16.1") && !InStr(title, "Not Responding"))
Output .= pid "`n"
}
tmpPids := StrSplit(Output, "`n")
for i, pid in tmpPids {
if (pid) {
inst := GetInstanceNum(pid)
OutputDebug, instance num: %inst%
orderedPIDs[inst] := pid
}
}
return orderedPIDs
}
HasGameSaved(idx, checkTitle) {
rawLogFile := StrReplace(SavesDirectories[idx], "saves", "logs\latest.log")
StringTrimRight, logFile, rawLogFile, 1
numLines := 0
Loop, Read, %logFile%
{
numLines += 1
}
saved := False
startTime := A_TickCount
Loop, Read, %logFile%
{
if ((numLines - A_Index) < 2)
{
if (checkTitle && InStr(A_LoopReadLine, "Stopping worker threads")) {
saved := True
break
}
if (!checkTitle && InStr(A_LoopReadLine, "Saving chunks for level 'ServerLevel") && InStr(A_LoopReadLine, "minecraft:the_end")) {
saved := True
break
}
}
}
return saved
}
RControl::Suspend ; Pause all macros
LAlt:: ; Reload if macro locks up
UnsuspendAll()
Reload
return
]:: ; Switches to wall
SwitchToWall()
return
#If WinActive("Windowed Projector")
LButton::
ResetInstance(WallCoords())
Return
RButton:: ; do not use "~" with mouse buttons
OutputDebug, pressed RButton
PlayInstance(WallCoords())
return
#IfWinActive, Minecraft
{
*CapsLock:: ExitWorld() ; Reset
*F12::
SetTitles()
return
*Tab:: ; BG Reset Instance 1
ResetInstance(1)
return
*\:: ; BG Reset Instance 2
ResetInstance(2)
return
*z:: ; BG Reset Instance 3
ResetInstance(3)
return
:: ; BG Reset Instance 4
ResetInstance(4)
return
}
@sheeperino
Copy link
Author

sheeperino commented Aug 11, 2021

Stable Version

Specnr Version

Changelog

The Wall

v0.19.3:

  • Added changes from Specnr's macro

v0.19.2:

  • Fully working version of the wall
  • Updated with Specnr Macro
  • Added Auto Resets every 5 minutes

v0.19.12:

  • Re-wrote macro for the wall
  • made the wall customisable

v0.19.11a:

  • Added break statements where needed
  • Works with pause on lost focus only

v0.19.11:

  • First separate patch of specnr macro
  • Fully working "the wall" (almost)
  • Hardcoded
  • May run into bugs
  • (added credits)

v0.19.1c (experimental):

  • Added the wall
  • Resets are broken for now

v0.19.1b (experimental):

  • Added an early version of the wall
  • Hardcoded functions

v0.19.1a:

  • Added options reset for render distance and FOV
  • Temporarily removed the count attempts functions
  • Made it so it unpauses on switch

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