Módulo de referência
O módulo que o site entrega, completo. A cada poucos segundos ele grava o estado do personagem em um arquivo e lê comandos de outro. O script envia os arquivos, então o cliente do jogo não faz requisições à internet.
Baixar o zip Licença MIT. Adapte para o seu servidor.
README.txt
Voidwatch module and upload scripts MIT licence
1. Close the game client.
2. Copy the "voidwatch" folder into the modules folder of your client: the folder that holds the
other modules (the ones named game_... and client_...).
3. Set up the upload script once, by double-clicking:
Windows: setup-windows.cmd
macOS: setup-mac.command (the first time: right-click it, pick Open, then Open again)
Linux: sh setup-linux.sh
It runs in the background and starts at every login from then on. A client that runs through Wine or
CrossOver uses the macOS or Linux setup.
4. Start the client and log in. A game message shows a code and a link. Open the link to try the
dashboard as a guest, or type the code in Settings, Clients on the website.
If the game message says the setup did not find the client, drop the folder it shows onto the setup file
(Windows) or drag it into the setup window (macOS), or run: sh setup-linux.sh "<folder>".
To remove the upload script: remove-windows.cmd, remove-mac.command or sh remove-linux.sh.
Adapting the module to a server: https://voidwatch.xyz/developers/new-server
voidwatch/voidwatch.otmod
Module
name: voidwatch
description: Writes the character state for the Voidwatch upload script and runs the commands it brings back
author: Voidwatch
website: https://voidwatch.xyz/developers
version: 0.3.0
sandboxed: true
autoload: true
autoload-priority: 1000
scripts: [ voidwatch ]
@onLoad: Voidwatch.init()
@onUnload: Voidwatch.terminate()
voidwatch/voidwatch.lua
-- Voidwatch reference module, feed schema vw.status/1. MIT licence.
--
-- The client makes no web requests. The module writes files into <write dir>/voidwatch/outbox and reads what the
-- upload script leaves in <write dir>/voidwatch/inbox:
-- outbox/status.json the character state: every 2 s while someone watches, every 30 s otherwise
-- outbox/capture.png a screenshot, when the last reply asked for one
-- outbox/pair-request.json who wants a pairing code (only until the script has a token)
-- outbox/results.json how the commands went
-- inbox/reply.json the last answer to status: statusEvery, captureEvery, commands, settings
-- inbox/pair.json the pairing code and link to show the player
-- inbox/commands.json commands to run
-- Server-specific data (routes, supplies, tasks) comes from adapters: see /developers/new-server.
Voidwatch = { adapters = {}, actions = {} }
local MODULE = 'voidwatch-feed/0.3.0'
local ROOT = '/voidwatch'
local LOOP_MS = 1000 -- how often the module looks at the reply; writing a file is cheap
local SERVER = '' -- set it, or let an adapter return server()
local statusEvent, lastShot, lastStatus, lastCommands, shownCode = nil, 0, 0, '', nil
local function write(path, text) pcall(g_resources.writeFileContents, ROOT .. path, text) end
local function read(path)
if not g_resources.fileExists(ROOT .. path) then return nil end
local ok, text = pcall(g_resources.readFileContents, ROOT .. path)
return ok and text or nil
end
local function readJson(path)
local text = read(path)
if not text then return nil end
local ok, value = pcall(json.decode, text)
return ok and value or nil
end
local function say(text)
if modules.game_textmessage and modules.game_textmessage.displayGameMessage then
modules.game_textmessage.displayGameMessage('[Voidwatch] ' .. text)
end
g_logger.info('[Voidwatch] ' .. text)
end
-- asks every adapter for a field; the first one that answers wins
local function ask(field, ...)
for _, a in ipairs(Voidwatch.adapters) do
if a[field] then
local ok, value = pcall(a[field], ...)
if ok and value ~= nil then return value end
end
end
return nil
end
local function state()
local p = g_game.getLocalPlayer()
if not p then return nil end
local pos = p:getPosition()
local skulls = {}
for _, c in ipairs(g_map.getSpectators(pos, false)) do
if c:isPlayer() and c ~= p and c:getSkull() ~= SkullNone then
local colours = { [SkullWhite] = 'white', [SkullRed] = 'red', [SkullBlack] = 'black', [SkullYellow] = 'yellow',
[SkullGreen] = 'green', [SkullOrange] = 'orange' }
if colours[c:getSkull()] then table.insert(skulls, { name = c:getName(), skull = colours[c:getSkull()] }) end
end
end
return {
schema = 'vw.status/1', module = MODULE, online = g_game.isOnline(),
character = { name = p:getName(), vocation = ask('vocation'), level = p:getLevel(), exp = p:getExperience() },
pos = { x = pos.x, y = pos.y, z = pos.z },
cap = math.floor(p:getFreeCapacity()), stamina = p:getStamina(),
expPerHour = ask('expPerHour'), route = ask('route'), routeOn = ask('routeOn'), label = ask('label'),
routes = ask('routes') or {}, supplies = ask('supplies') or {}, tasks = ask('tasks') or {}, loot = ask('loot') or {},
lootHours = ask('lootHours'), skulls = skulls, blessed = ask('blessed'), redemptionLeft = ask('redemptionLeft'),
}
end
local function pairIfNeeded(s)
local pair = readJson('/inbox/pair.json')
if pair and pair.paired then return end
write('/outbox/pair-request.json', json.encode({ character = s.character.name, server = ask('server') or SERVER, module = MODULE }))
if pair and pair.code and pair.code ~= shownCode then
shownCode = pair.code
say('pairing code ' .. pair.code .. ', or open ' .. (pair.url or 'the website'))
end
end
local function runCommands()
local text = read('/inbox/commands.json')
if not text or text == lastCommands then return end
lastCommands = text
local ok, list = pcall(json.decode, text)
if not ok or type(list) ~= 'table' then return end
local results = {}
for _, c in ipairs(list) do
local fn = Voidwatch.actions[c.action] or ask('action', c.action)
local done, message = false, 'not supported on this client'
if fn then
local okRun, res, msg = pcall(fn, c.args or {})
done, message = okRun and res ~= false, okRun and msg or tostring(res)
end
table.insert(results, { id = c.id, ok = done, message = message })
end
write('/outbox/results.json', json.encode(results))
end
local function tick()
statusEvent = scheduleEvent(tick, LOOP_MS)
if not g_game.isOnline() then return end
local reply = readJson('/inbox/reply.json') or {}
if g_clock.millis() - lastStatus >= (reply.statusEvery or 30) * 1000 or reply.statusEvery == nil then
local s = state()
if not s then return end
lastStatus = g_clock.millis()
pairIfNeeded(s)
write('/outbox/status.json', json.encode(s))
end
local every = (reply.captureEvery or 60) * 1000
if g_clock.millis() - lastShot >= every then
lastShot = g_clock.millis()
g_app.doScreenshot(ROOT .. '/outbox/capture.png')
end
runCommands()
end
-- actions every client can run without an adapter
Voidwatch.actions.capture = function() lastShot = 0; return true end
Voidwatch.actions.logout_in_pz = function()
local p = g_game.getLocalPlayer()
if p and p:isInProtectionZone() then g_game.safeLogout(); return true end
return false, 'not in a protection zone'
end
function Voidwatch.init()
g_resources.makeDir(ROOT)
g_resources.makeDir(ROOT .. '/outbox')
g_resources.makeDir(ROOT .. '/inbox')
statusEvent = scheduleEvent(tick, 1000)
-- the setup finds clients in the usual places; for any other place it needs this folder once
scheduleEvent(function()
local pair = readJson('/inbox/pair.json')
if not pair then say('run the Voidwatch setup once. If it does not find this client, give it this folder: ' .. g_resources.getWriteDir() .. 'voidwatch') end
end, 8000)
end
function Voidwatch.terminate()
if statusEvent then removeEvent(statusEvent) end
statusEvent = nil
end
setup-windows.cmd
@echo off
rem Voidwatch setup for Windows: copies the upload script, starts it now and at every login. No admin rights needed.
rem Drop the folder the game message showed onto this file to add a client that is not in the usual place.
setlocal
set "DEST=%LOCALAPPDATA%\Voidwatch"
rem the upload script ran as "Emberwatch" before the rename; stop that one too
powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='powershell.exe'\" | Where-Object { $_.CommandLine -like '*Emberwatch\upload.ps1*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
del "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Emberwatch upload.lnk" 2>nul
rmdir /S /Q "%LOCALAPPDATA%\Emberwatch" 2>nul
mkdir "%DEST%" 2>nul
copy /Y "%~dp0upload\upload.ps1" "%DEST%\upload.ps1" >nul
if not "%~1"=="" echo %~1>>"%DEST%\folders.txt"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$s = (New-Object -ComObject WScript.Shell).CreateShortcut([Environment]::GetFolderPath('Startup') + '\Voidwatch upload.lnk'); $s.TargetPath = 'powershell.exe'; $s.Arguments = '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"' + $env:LOCALAPPDATA + '\Voidwatch\upload.ps1\"'; $s.WindowStyle = 7; $s.Save()"
start "" powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%DEST%\upload.ps1"
echo.
echo Voidwatch is set up. It runs in the background now and starts with Windows from now on.
echo Start the game and log in: a game message shows a code and a link.
echo.
pause
setup-mac.command
#!/bin/sh
# Voidwatch setup for macOS, also for clients in CrossOver or Wine: copies the upload script, starts it now and at
# every login. Double-click it. The first time, macOS may ask: right-click the file, pick Open, then Open again.
set -eu
DEST="$HOME/Library/Application Support/Voidwatch"
PLIST="$HOME/Library/LaunchAgents/xyz.voidwatch.upload.plist"
# the upload script ran as "Emberwatch" before the rename; stop that one too
launchctl unload "$HOME/Library/LaunchAgents/online.funyo.emberwatch.plist" 2>/dev/null || true
rm -f "$HOME/Library/LaunchAgents/online.funyo.emberwatch.plist"
pkill -f "Emberwatch/upload.sh" 2>/dev/null || true
rm -rf "$HOME/Library/Application Support/Emberwatch"
mkdir -p "$DEST" "$HOME/Library/LaunchAgents"
cp "$(dirname "$0")/upload/upload.sh" "$DEST/upload.sh"
echo "Drag the folder the game message showed into this window and press Enter,"
printf "or just press Enter to look in the usual places: "
read -r folder || folder=""
folder=$(printf '%s' "$folder" | sed "s/^'//; s/'$//; s/\\\\ / /g")
[ -n "$folder" ] && printf '%s\n' "$folder" >> "$DEST/folders.txt"
cat > "$PLIST" <<PL
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>xyz.voidwatch.upload</string>
<key>ProgramArguments</key><array><string>/bin/sh</string><string>$DEST/upload.sh</string></array>
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
</dict></plist>
PL
launchctl unload "$PLIST" 2>/dev/null || true
launchctl load "$PLIST"
echo
echo "Voidwatch is set up. It runs in the background now and starts with your Mac from now on."
echo "Start the game and log in: a game message shows a code and a link. You can close this window."
setup-linux.sh
#!/bin/sh
# Voidwatch setup for Linux, also for clients in Wine: copies the upload script and runs it as a user service, now and
# at every login. Run: sh setup-linux.sh [the folder the game message showed]
set -eu
DEST="${XDG_DATA_HOME:-$HOME/.local/share}/voidwatch"
UNIT="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/voidwatch.service"
# the upload script ran as "emberwatch" before the rename; stop that one too
systemctl --user disable --now emberwatch.service 2>/dev/null || true
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/emberwatch.service"
rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/emberwatch"
mkdir -p "$DEST" "$(dirname "$UNIT")"
cp "$(dirname "$0")/upload/upload.sh" "$DEST/upload.sh"
[ $# -gt 0 ] && printf '%s\n' "$1" >> "$DEST/folders.txt"
cat > "$UNIT" <<U
[Unit]
Description=Voidwatch upload script
[Service]
ExecStart=/bin/sh $DEST/upload.sh
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
U
systemctl --user daemon-reload
systemctl --user enable --now voidwatch.service
echo "Voidwatch is set up and starts at every login. Start the game and log in: a game message shows a code and a link."
upload/upload.ps1
# Voidwatch upload script for Windows. It sends what the module writes and brings back the replies. MIT licence.
# powershell -ExecutionPolicy Bypass -File upload.ps1 "<the voidwatch folder in the client's write directory>"
# The module shows the exact folder in a game message.
param([string]$Dir = '')
$Home_ = Join-Path $env:LOCALAPPDATA 'Voidwatch'
# Without a folder: watch every client on this computer. The usual places are searched every 30 s, plus the folders
# listed in folders.txt next to this script (setup adds the one you drop on it). Each client gets its own hidden worker.
if (-not $Dir) {
New-Item -ItemType Directory -Force -Path $Home_ | Out-Null
while ($true) {
$found = @(Get-ChildItem (Join-Path $env:APPDATA 'OTClientV8') -Directory -ErrorAction SilentlyContinue |
ForEach-Object { Join-Path $_.FullName 'voidwatch' } | Where-Object { Test-Path (Join-Path $_ 'outbox') })
$list = Join-Path $Home_ 'folders.txt'
if (Test-Path $list) { $found += Get-Content $list | Where-Object { $_ -and (Test-Path $_) } }
foreach ($d in ($found | Sort-Object -Unique)) {
$pidFile = Join-Path $d '.pid'
$alive = $false
if (Test-Path $pidFile) { $alive = [bool](Get-Process -Id ([int](Get-Content $pidFile)) -ErrorAction SilentlyContinue) }
if (-not $alive) {
$p = Start-Process powershell -WindowStyle Hidden -PassThru -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $PSCommandPath, $d)
Set-Content -Path $pidFile -Value $p.Id
}
}
Start-Sleep -Seconds 30
}
}
$Api = if ($env:VOIDWATCH_API) { $env:VOIDWATCH_API } else { 'https://voidwatch.xyz/api/v1' }
$Out = Join-Path $Dir 'outbox'; $In = Join-Path $Dir 'inbox'; $TokenFile = Join-Path $Dir '.token'
New-Item -ItemType Directory -Force -Path $In | Out-Null
$lastCapture = [datetime]::MinValue; $lastMinimap = [datetime]::MinValue
$lastStatus = [datetime]::MinValue; $minimapCheck = [datetime]::MinValue
$statusEvery = 30; $minimapOn = $true
function Headers { @{ Authorization = 'Bearer ' + (Get-Content $TokenFile -Raw).Trim() } }
function Send-Status {
$reply = Invoke-RestMethod -Method Post -Uri "$Api/status" -Headers (Headers) -ContentType 'application/json' -InFile "$Out\status.json" -TimeoutSec 10
$reply | ConvertTo-Json -Depth 5 | Set-Content "$In\reply.json"
'{"paired":true}' | Set-Content "$In\pair.json"
$script:statusEvery = [int]$reply.statusEvery
$script:minimapOn = $reply.settings.minimap -ne $false
$script:lastStatus = Get-Date
if ($reply.commands -gt 0) {
Invoke-RestMethod -Uri "$Api/commands" -Headers (Headers) -TimeoutSec 10 | ConvertTo-Json -Depth 5 -AsArray | Set-Content "$In\commands.json"
}
}
while ($true) {
try {
if (-not (Test-Path $TokenFile)) {
if (Test-Path "$Out\pair-request.json") {
$r = Invoke-RestMethod -Method Post -Uri "$Api/pair" -ContentType 'application/json' -InFile "$Out\pair-request.json" -TimeoutSec 10
Set-Content -Path $TokenFile -Value $r.token -NoNewline
@{ code = $r.code; url = $r.url; expiresAt = $r.expiresAt } | ConvertTo-Json | Set-Content "$In\pair.json"
}
Start-Sleep -Seconds 1
continue
}
if ((Test-Path "$Out\status.json") -and ((Get-Date) - $lastStatus).TotalSeconds -ge $statusEvery) { Send-Status }
$shot = Get-Item "$Out\capture.png" -ErrorAction SilentlyContinue
if ($shot -and $shot.LastWriteTime -ne $lastCapture) {
Invoke-RestMethod -Method Post -Uri "$Api/capture" -Headers (Headers) -ContentType 'image/png' -InFile $shot.FullName -TimeoutSec 20 | Out-Null
$lastCapture = $shot.LastWriteTime
}
if (Test-Path "$Out\results.json") {
foreach ($r in (Get-Content "$Out\results.json" -Raw | ConvertFrom-Json)) {
$body = @{ ok = $r.ok; message = $r.message } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "$Api/commands/$($r.id)/result" -Headers (Headers) -ContentType 'application/json' -Body $body -TimeoutSec 10 | Out-Null
}
Remove-Item "$Out\results.json"
}
# the client's own minimap, one folder up; it changes slowly, so look at it once a minute
if ($minimapOn -and ((Get-Date) - $minimapCheck).TotalSeconds -ge 60) {
$minimapCheck = Get-Date
$map = Get-ChildItem (Join-Path $Dir '..') -Filter 'minimap*.otmm' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($map -and $map.LastWriteTime -ne $lastMinimap) {
Invoke-RestMethod -Method Post -Uri "$Api/minimap" -Headers (Headers) -ContentType 'application/octet-stream' -InFile $map.FullName -TimeoutSec 60 | Out-Null
$lastMinimap = $map.LastWriteTime
}
}
if ($statusEvery -le 2) {
Start-Sleep -Seconds 1
} else {
# wait until someone opens the character, an alert fires or a command arrives; returns at once when that happens
$w = Invoke-RestMethod -Uri "$Api/wait?timeout=25" -Headers (Headers) -TimeoutSec 30
if ($w.wake) { $lastStatus = [datetime]::MinValue }
}
} catch { Write-Host (Get-Date -Format 'HH:mm:ss') $_.Exception.Message; Start-Sleep -Seconds 2 }
}
upload/upload.sh
#!/bin/sh
# Voidwatch upload script for macOS and Linux (also for clients that run through Wine or CrossOver).
# It sends what the module writes and brings back the replies. MIT licence.
# sh upload.sh "<the voidwatch folder in the client's write directory>"
# The module shows the exact folder in a game message.
set -u
API=${VOIDWATCH_API:-https://voidwatch.xyz/api/v1}
HOME_DIR="$HOME/Library/Application Support/Voidwatch"
[ -d "$HOME_DIR" ] || HOME_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/voidwatch"
# Without a folder: watch every client on this computer. The usual places are searched every 30 s, plus the folders
# listed in folders.txt next to this script (setup adds the one you drag in). Each client gets its own worker.
if [ $# -eq 0 ]; then
mkdir -p "$HOME_DIR"
while :; do
for d in "$HOME/Library/Application Support/OTClientV8"/*/voidwatch "$HOME/.otclientv8"/*/voidwatch "$HOME/.local/share/otclientv8"/*/voidwatch \
"$HOME/Library/Application Support/CrossOver/Bottles"/*/drive_c/users/*/AppData/Roaming/OTClientV8/*/voidwatch \
"$HOME/.wine/drive_c/users"/*/AppData/Roaming/OTClientV8/*/voidwatch; do
[ -d "$d/outbox" ] && printf '%s\n' "$d"
done > "$HOME_DIR/.found" 2>/dev/null
[ -f "$HOME_DIR/folders.txt" ] && cat "$HOME_DIR/folders.txt" >> "$HOME_DIR/.found"
while IFS= read -r d; do
[ -d "$d" ] || continue
pid=$(cat "$d/.pid" 2>/dev/null || echo 0)
kill -0 "$pid" 2>/dev/null || { sh "$0" "$d" >/dev/null 2>&1 & echo $! > "$d/.pid"; }
done < "$HOME_DIR/.found"
sleep 30
done
fi
DIR=$1
OUT="$DIR/outbox" IN="$DIR/inbox" TOKEN_FILE="$DIR/.token"
[ -f "$TOKEN_FILE" ] && chmod 600 "$TOKEN_FILE"
mkdir -p "$IN"
last_capture=0 last_status=0 last_minimap=0 minimap_check=0 status_every=30
auth() { printf 'Authorization: Bearer %s' "$(cat "$TOKEN_FILE")"; }
post_json() { curl -fsS -m 10 -X POST -H "Content-Type: application/json" -H "$(auth)" --data-binary "@$2" "$API$1"; }
field() { printf '%s' "$1" | sed -n "s/.*\"$2\":\([0-9a-z]*\).*/\1/p"; }
mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1"; }
send_status() {
reply=$(post_json /status "$OUT/status.json") || return
printf '%s' "$reply" > "$IN/reply.json"
printf '{"paired":true}' > "$IN/pair.json"
status_every=$(field "$reply" statusEvery); status_every=${status_every:-30}
last_status=$(date +%s)
case "$reply" in *'"commands":0'*) ;; *)
curl -fsS -m 10 -H "$(auth)" "$API/commands" > "$IN/commands.json.tmp" && mv "$IN/commands.json.tmp" "$IN/commands.json" ;;
esac
case "$reply" in *'"minimap":false'*) minimap_on=0 ;; *) minimap_on=1 ;; esac
}
while :; do
if [ ! -s "$TOKEN_FILE" ]; then
if [ -f "$OUT/pair-request.json" ]; then
reply=$(curl -fsS -m 10 -X POST -H "Content-Type: application/json" --data-binary "@$OUT/pair-request.json" "$API/pair") && {
(umask 077; printf '%s' "$reply" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p' > "$TOKEN_FILE")
printf '%s' "$reply" | sed 's/"token":"[^"]*",\{0,1\}//' > "$IN/pair.json"
}
fi
sleep 1
continue
fi
now=$(date +%s)
if [ -f "$OUT/status.json" ] && [ $((now - last_status)) -ge "$status_every" ]; then send_status; fi
if [ -f "$OUT/capture.png" ]; then
m=$(mtime "$OUT/capture.png")
if [ "$m" != "$last_capture" ]; then
curl -fsS -m 20 -X POST -H "Content-Type: image/png" -H "$(auth)" --data-binary "@$OUT/capture.png" "$API/capture" >/dev/null && last_capture=$m
fi
fi
if [ -f "$OUT/results.json" ]; then
# one result per command: {"id":1,"ok":true,"message":"..."}
sed 's/},{/}\n{/g; s/^\[//; s/\]$//' "$OUT/results.json" | while IFS= read -r r; do
id=$(printf '%s' "$r" | sed -n 's/.*"id":\([0-9]*\).*/\1/p')
[ -n "$id" ] && printf '%s' "$r" > "$OUT/.result" && post_json "/commands/$id/result" "$OUT/.result" >/dev/null
done
rm -f "$OUT/results.json" "$OUT/.result"
fi
# the client's own minimap, one folder up; it changes slowly, so look at it once a minute
if [ "${minimap_on:-1}" = 1 ] && [ $((now - minimap_check)) -ge 60 ]; then
minimap_check=$now
map="" best=0
for f in "$DIR"/../minimap*.otmm; do
[ -f "$f" ] || continue
m=$(mtime "$f"); if [ "$m" -gt "$best" ]; then map=$f best=$m; fi
done
if [ -n "$map" ] && [ "$best" != "$last_minimap" ]; then
curl -fsS -m 60 -X POST -H "Content-Type: application/octet-stream" -H "$(auth)" --data-binary "@$map" "$API/minimap" >/dev/null \
&& last_minimap=$(mtime "$map")
fi
fi
if [ "$status_every" -le 2 ]; then
sleep 1
else
# wait until someone opens the character, an alert fires or a command arrives; returns at once when that happens
reply=$(curl -fsS -m 30 -H "$(auth)" "$API/wait?timeout=25") && case "$reply" in *'"wake":true'*) last_status=0 ;; esac
fi
done
remove-windows.cmd
@echo off
rem Removes the Voidwatch upload script from Windows: stops it, removes it from the startup items, deletes its files.
powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='powershell.exe'\" | Where-Object { $_.CommandLine -like '*Voidwatch\upload.ps1*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
rem the upload script ran as "Emberwatch" before the rename; stop that one too
powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"Name='powershell.exe'\" | Where-Object { $_.CommandLine -like '*Emberwatch\upload.ps1*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"
del "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Emberwatch upload.lnk" 2>nul
rmdir /S /Q "%LOCALAPPDATA%\Emberwatch" 2>nul
del "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Voidwatch upload.lnk" 2>nul
rmdir /S /Q "%LOCALAPPDATA%\Voidwatch" 2>nul
echo Voidwatch upload script removed. The module in your game client stays until you delete its folder.
pause
remove-mac.command
#!/bin/sh
# Removes the Voidwatch upload script from macOS: stops it, removes the login item, deletes its files.
# the upload script ran as "Emberwatch" before the rename; stop that one too
launchctl unload "$HOME/Library/LaunchAgents/online.funyo.emberwatch.plist" 2>/dev/null
rm -f "$HOME/Library/LaunchAgents/online.funyo.emberwatch.plist"
pkill -f "Emberwatch/upload.sh" 2>/dev/null
rm -rf "$HOME/Library/Application Support/Emberwatch"
PLIST="$HOME/Library/LaunchAgents/xyz.voidwatch.upload.plist"
launchctl unload "$PLIST" 2>/dev/null
rm -f "$PLIST"
pkill -f "Voidwatch/upload.sh" 2>/dev/null
rm -rf "$HOME/Library/Application Support/Voidwatch"
echo "Voidwatch upload script removed. The module in your game client stays until you delete its folder."
remove-linux.sh
#!/bin/sh
# Removes the Voidwatch upload script from Linux: stops the user service and deletes its files.
# the upload script ran as "emberwatch" before the rename; stop that one too
systemctl --user disable --now emberwatch.service 2>/dev/null
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/emberwatch.service"
rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/emberwatch"
systemctl --user disable --now voidwatch.service 2>/dev/null
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/voidwatch.service"
rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/voidwatch"
echo "Voidwatch upload script removed. The module in your game client stays until you delete its folder."