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
31 changes: 31 additions & 0 deletions scripts/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Param(
[Parameter(Mandatory=$false)]
[Switch] $clean,

[Parameter(Mandatory=$false)]
[Switch] $help
)

if ($help -eq $true) {
Write-Output "`"Build`" - Copiles your mod into a `".so`" or a `".a`" library"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix help text typo.

Line 10 says “Copiles”; should be “Compiles”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build.ps1` at line 10, Fix the typo in the build script help text:
locate the Write-Output call that prints the string "`"Build`" - Copiles your
mod into a `".so`" or a `".a`" library" and change "Copiles" to "Compiles" so
the message reads "`"Build`" - Compiles your mod into a `".so`" or a `".a`"
library".

Write-Output "`n-- Arguments --`n"

Write-Output "-Clean `t`t Deletes the `"build`" folder, so that the entire library is rebuilt"

exit
}

# if user specified clean, remove all build files
if ($clean.IsPresent) {
if (Test-Path -Path "build") {
remove-item build -R
}
}


if (($clean.IsPresent) -or (-not (Test-Path -Path "build"))) {
new-item -Path build -ItemType Directory
}

& cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -B build
& cmake --build ./build
Comment on lines +20 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat scripts/build.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 803


Anchor build paths to script location and check CMake configure exit code before building.

The script uses relative paths (build, ./build) that resolve to the current working directory, not the script location. If invoked from a different directory, it targets the wrong tree. Additionally, the CMake build command (line 31) executes even if the configure step (line 30) fails, causing confusing secondary errors.

Suggested fix
+ $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
+ $buildDir = Join-Path $repoRoot "build"
+
  if ($clean.IsPresent) {
-    if (Test-Path -Path "build") {
-        remove-item build -R
+    if (Test-Path -Path $buildDir) {
+        Remove-Item $buildDir -Recurse -Force
     }
  }
 
- if (($clean.IsPresent) -or (-not (Test-Path -Path "build"))) {
-    new-item -Path build -ItemType Directory
+ if (-not (Test-Path -Path $buildDir)) {
+    New-Item -Path $buildDir -ItemType Directory | Out-Null
  } 
 
- & cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -B build
- & cmake --build ./build
+ & cmake -S $repoRoot -B $buildDir -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & cmake --build $buildDir
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build.ps1` around lines 20 - 31, The script uses relative paths and
always runs the build step even if CMake configure fails; update it to resolve
the build directory relative to the script location (use $PSScriptRoot or
$MyInvocation.MyCommand.Definition and Join-Path to form the "build" path) and
replace literal "build" and "./build" with that resolved path when calling
Test-Path, Remove-Item, New-Item and the cmake commands; after running the
configure command (cmake -B <resolvedBuild> ...), check its result via
$LASTEXITCODE or $? and only run the build step (cmake --build <resolvedBuild>)
when the configure succeeded, returning a non-zero exit code on failure.

79 changes: 79 additions & 0 deletions scripts/copy.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
Param(
[Parameter(Mandatory=$false)]
[Switch] $clean,

[Parameter(Mandatory=$false)]
[Switch] $log,

[Parameter(Mandatory=$false)]
[Switch] $useDebug,

[Parameter(Mandatory=$false)]
[Switch] $self,

[Parameter(Mandatory=$false)]
[Switch] $all,

[Parameter(Mandatory=$false)]
[String] $custom="",

[Parameter(Mandatory=$false)]
[Switch] $file,

[Parameter(Mandatory=$false)]
[Switch] $help
)

if ($help -eq $true) {
Write-Output "`"Copy`" - Builds and copies your mod to your quest, and also starts Beat Saber with optional logging"
Write-Output "`n-- Arguments --`n"

Write-Output "-Clean `t`t Performs a clean build (equvilant to running `"build -clean`")"
Write-Output "-UseDebug `t Copies the debug version of the mod to your quest"
Write-Output "-Log `t`t Logs Beat Saber using the `"Start-Logging`" command"

Write-Output "`n-- Logging Arguments --`n"

& $PSScriptRoot/start-logging.ps1 -help -excludeHeader

exit
}

& $PSScriptRoot/build.ps1 -clean:$clean

if ($LASTEXITCODE -ne 0) {
Write-Output "Failed to build, exiting..."
exit $LASTEXITCODE
}

& $PSScriptRoot/validate-modjson.ps1
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$modJson = Get-Content "./mod.json" -Raw | ConvertFrom-Json

$modFiles = $modJson.modFiles
$lateModFiles = $modJson.lateModFiles

foreach ($fileName in $modFiles) {
if ($useDebug -eq $true) {
& adb push build/debug/$fileName /sdcard/ModData/com.beatgames.beatsaber/Modloader/early_mods/$fileName
} else {
& adb push build/$fileName /sdcard/ModData/com.beatgames.beatsaber/Modloader/early_mods/$fileName
}
}

foreach ($fileName in $lateModFiles) {
if ($useDebug -eq $true) {
& adb push build/debug/$fileName /sdcard/ModData/com.beatgames.beatsaber/Modloader/mods/$fileName
} else {
& adb push build/$fileName /sdcard/ModData/com.beatgames.beatsaber/Modloader/mods/$fileName
Comment on lines +53 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the repository structure
git ls-files | head -20

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 509


🏁 Script executed:

# Read the full scripts/copy.ps1 file to understand context
wc -l scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 98


🏁 Script executed:

# Read the relevant section and surrounding context
sed -n '40,75p' scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1079


🏁 Script executed:

# Find mod.json location
find . -name "mod.json" -type f

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 61


🏁 Script executed:

# Check the build.ps1 script to understand build output paths
sed -n '1,50p' scripts/build.ps1 | head -40

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 803


🏁 Script executed:

# Check if mod.json is in scripts directory
ls -la scripts/ | grep -E "(mod\.json|\.json)"

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 61


🏁 Script executed:

# Read the beginning of copy.ps1 to see initialization and parameter handling
sed -n '1,40p' scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1095


🏁 Script executed:

# Check if mod.json exists in the repo (broader search)
find . -name "*.json" -type f | grep -i mod

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 98


🏁 Script executed:

# Check full repository structure at root and scripts
ls -la scripts/ | head -30

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 717


🏁 Script executed:

# Read more of copy.ps1 to see if working directory is set
sed -n '40,60p' scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 619


🏁 Script executed:

# Check the README or any documentation on how to run scripts
grep -r "copy.ps1\|\.ps1" README.md 2>/dev/null | head -20

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 61


🏁 Script executed:

# Check if there's a script that calls copy.ps1 to see intended cwd
grep -r "copy\.ps1" . --include="*.ps1" --include="*.yml" --include="*.yaml" --include="*.md"

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 61


🏁 Script executed:

# Check if other PowerShell scripts follow similar patterns
rg 'Get-Content.*"\./[^"]*"' scripts/ --no-heading

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 155


🏁 Script executed:

# Check if any validation script gives hints about where mod.json should be
cat scripts/validate-modjson.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1149


🏁 Script executed:

# Check the entire copy.ps1 to understand full context
cat -n scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 2779


Use repo-rooted file paths for mod.json and build artifacts.

Lines 53, 60, 62, 68, and 70 use working-directory-relative paths. When copy.ps1 is invoked from directories other than the repository root, these relative path lookups will fail. Use $PSScriptRoot/../mod.json for the mod.json reference and $PSScriptRoot/../build for build artifacts to match the pattern already used for script invocations on lines 42, 49, and 74.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/copy.ps1` around lines 53 - 70, The script uses
working-directory-relative paths for mod.json and build artifacts which break
when copy.ps1 is run outside the repo root; update the references that
read/consume $modJson, $modFiles, $lateModFiles and the adb push invocations
that use build/... and build/debug/... to instead build repo-rooted paths using
the script root (use $PSScriptRoot to prefix ../mod.json and ../build or
../build/debug) so every Get-Content "./mod.json" and all adb push sources point
to $PSScriptRoot/../mod.json and $PSScriptRoot/../build( /debug)/<fileName>
respectively while keeping the $useDebug conditional and existing foreach loops
unchanged.

}
}

& $PSScriptRoot/restart-game.ps1

Comment on lines +58 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/copy.ps1 | head -100

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 2779


🏁 Script executed:

grep -n "ErrorActionPreference\|$ErrorActionPreference" scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 2454


🏁 Script executed:

cat scripts/restart-game.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 208


Check exit codes for adb push and restart-game.ps1 calls.

Lines 58–75 do not check $LASTEXITCODE after adb push or restart-game.ps1 operations. If a push fails (device disconnect, missing file, etc.), the loop continues silently and the script exits with code 0, creating a false sense of success. Add $LASTEXITCODE checks after each push and after the restart call, exiting non-zero on failure—consistent with how this script handles the build and validation steps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/copy.ps1` around lines 58 - 75, After each invocation of adb push
inside the foreach loops over $modFiles and $lateModFiles (the branches that use
$useDebug to pick build/debug vs build), check $LASTEXITCODE and if it is
non-zero log an error mentioning the failing file and exit with a non-zero code;
do the same immediately after calling & $PSScriptRoot/restart-game.ps1 (check
$LASTEXITCODE, log and exit non-zero on failure) so failures don’t get swallowed
and the script returns a failing status.

if ($log -eq $true) {
& adb logcat -c
& $PSScriptRoot/start-logging.ps1 -self:$self -all:$all -custom:$custom -file:$file
}
77 changes: 77 additions & 0 deletions scripts/createqmod.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
Param(
[Parameter(Mandatory=$false)]
[String] $qmodName="",

[Parameter(Mandatory=$false)]
[Switch] $help
)

if ($help -eq $true) {
Write-Output "`"createqmod`" - Creates a .qmod file with your compiled libraries and mod.json."
Write-Output "`n-- Arguments --`n"

Write-Output "-QmodName `t The file name of your qmod"

exit
}

$mod = "./mod.json"

& $PSScriptRoot/validate-modjson.ps1
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$modJson = Get-Content $mod -Raw | ConvertFrom-Json

if ($qmodName -eq "") {
$qmodName = $modJson.name
}

$filelist = @($mod)

$cover = "./" + $modJson.coverImage
if ((-not ($cover -eq "./")) -and (Test-Path $cover)) {
$filelist += ,$cover
}

foreach ($mod in $modJson.modFiles) {
$path = "./build/" + $mod
if (-not (Test-Path $path)) {
$path = "./extern/libs/" + $mod
Comment on lines +18 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine the createqmod.ps1 script
find . -name "createqmod.ps1" -type f

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 103


🏁 Script executed:

# Read the full script to understand the context
cat -n scripts/createqmod.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 2368


🏁 Script executed:

# Check if there are other scripts in the same directory that might give context
ls -la scripts/

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 717


🏁 Script executed:

# Look for README or documentation
find . -maxdepth 2 -type f \( -name "README*" -o -name "*.md" \) | head -20

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 117


🏁 Script executed:

# Check build.ps1 to see how paths are handled there
cat -n scripts/build.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1020


🏁 Script executed:

# Check if there are any CI/workflow files that show how this script is called
find . -path "*/.github/workflows/*.yml" -o -path "*/CI/*" -o -path "*/.gitlab-ci.yml" 2>/dev/null | head -10

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 214


🏁 Script executed:

# Check the workflow files to see how createqmod.ps1 is invoked
cat -n .github/workflows/Pre-Release.yml

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 5502


🏁 Script executed:

cat -n .github/workflows/Release.yml

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 5862


🏁 Script executed:

# Also check README to see if there's documentation about script usage
cat -n README.md | head -100

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 669


Resolve package input paths from repo root for portability.

The script mixes approaches: line 20 correctly uses $PSScriptRoot to invoke validate-modjson.ps1, but lines 18+ revert to cwd-relative paths (./mod.json, ./build/, ./extern/libs/), breaking if invoked from a directory other than the repository root. This inconsistency makes the script fragile for local development. Refactor paths to use $PSScriptRoot as the base, e.g., Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..")) "mod.json" or similar, for consistent portability across all path operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/createqmod.ps1` around lines 18 - 40, The script uses cwd-relative
paths for $mod, $cover and the foreach loop over $modJson.modFiles which breaks
when not run from the repo root; change all path constructions to be anchored to
$PSScriptRoot (or the repo root resolved from it) instead of "./"—e.g., resolve
$mod via Join-Path/Resolve-Path with $PSScriptRoot before calling Get-Content,
build $cover and each $path for "./build/" and "./extern/libs/" from that same
base, and use the resulting absolute paths for Test-Path and to populate
$filelist so all operations (Get-Content, Test-Path, file additions) are
repo-root portable.

}
if (-not (Test-Path $path)) {
Write-Output "Error: could not find dependency: $path"
exit 1
}
$filelist += $path
}

foreach ($mod in $modJson.lateModFiles) {
$path = "./build/" + $mod
if (-not (Test-Path $path)) {
$path = "./extern/libs/" + $mod
}
if (-not (Test-Path $path)) {
Write-Output "Error: could not find dependency: $path"
exit 1
}
$filelist += $path
}

foreach ($lib in $modJson.libraryFiles) {
$path = "./build/" + $lib
if (-not (Test-Path $path)) {
$path = "./extern/libs/" + $lib
}
if (-not (Test-Path $path)) {
Write-Output "Error: could not find dependency: $path"
exit 1
}
$filelist += $path
}

$zip = $qmodName + ".zip"
$qmod = $qmodName + ".qmod"

Compress-Archive -Path $filelist -DestinationPath $zip -Update
Move-Item $zip $qmod -Force
Comment on lines +76 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file and the specific lines mentioned
if [ -f "scripts/createqmod.ps1" ]; then
    echo "=== File found, reading lines 70-85 for context ==="
    sed -n '70,85p' scripts/createqmod.ps1
else
    echo "=== Looking for the file ==="
    find . -name "createqmod.ps1" -type f
fi

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 302


🌐 Web query:

PowerShell Compress-Archive -Update flag behavior retention stale files

💡 Result:

Compress-Archive -Update does not do “sync” semantics. It only:

  • Replaces archive entries when the source file is a newer version of an existing entry, and
  • Adds files that are new in the source path(s).
    (This is exactly how Microsoft documents -Update in Example 9: “update with newer versions … and new files … are added.”) [1]

So if you delete a file from the source folder, -Update will not remove the corresponding entry from the .zip—those become stale files that remain in the archive. This mirrors standard ZIP “update” behavior and is a commonly observed gotcha when trying to keep an archive matched to a directory [2].

How to avoid retaining stale files

  • Recreate the archive (delete the zip, then run Compress-Archive without -Update) when you need an exact mirror.
  • Or manipulate entries directly via .NET (System.IO.Compression.ZipArchive) if you must delete specific entries without rebuilding.

Extra gotcha: -Update can delete the zip in some failure/empty-input cases

If the -Path you pass results in zero files, there are scenarios where Compress-Archive -Update can effectively wipe/delete the destination archive (reported/diagnosed by users) [3]. If your file set can be empty, guard it (e.g., only call Compress-Archive when you have at least one file).

Sources:
[1] Microsoft Learn – Compress-Archive docs (Example 9, -Update behavior) (learn.microsoft.com)
[2] Stack Overflow – ZIP update (-u) keeps files that were deleted from the directory (analogous “update doesn’t remove”) (stackoverflow.com)
[3] Stack Overflow – Compress-Archive -Update with empty input can erase/remove the archive (stackoverflow.com)

Citations:


Avoid Compress-Archive -Update for release artifacts—stale files from prior runs will persist in the package.

The -Update flag only adds new files and replaces newer versions of existing entries; it does not remove files that were deleted from the source. This means removed dependencies or old mod files from previous packaging runs remain in the .qmod, which is unsafe for releases. Delete and recreate the archive instead.

Suggested fix
 $zip = $qmodName + ".zip"
 $qmod = $qmodName + ".qmod"
 
-Compress-Archive -Path $filelist -DestinationPath $zip -Update
+if (Test-Path $zip) { Remove-Item $zip -Force }
+if (Test-Path $qmod) { Remove-Item $qmod -Force }
+Compress-Archive -Path $filelist -DestinationPath $zip
 Move-Item $zip $qmod -Force
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/createqmod.ps1` around lines 76 - 77, The script uses
Compress-Archive with -Update which preserves files removed from the source;
change the logic around the $zip/$qmod creation to remove any existing archive
first and recreate it (e.g., if (Test-Path $zip) { Remove-Item $zip -Force }
then call Compress-Archive -Path $filelist -DestinationPath $zip without
-Update, and finally Move-Item $zip $qmod -Force), referencing the existing
$zip, $qmod and $filelist variables so the created .qmod contains only current
files.

52 changes: 52 additions & 0 deletions scripts/pull-tombstone.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Param(
[Parameter(Mandatory=$false)]
[String] $fileName = "RecentCrash.log",

[Parameter(Mandatory=$false)]
[Switch] $analyze,

[Parameter(Mandatory=$false)]
[Switch] $help
)

if ($help -eq $true) {
Write-Output "`"Pull-Tombstone`" - Finds and pulls the most recent tombstone from your quest, optionally analyzing it with ndk-stack"
Write-Output "`n-- Arguments --`n"

Write-Output "-FileName `t The name for the output file, defaulting to RecentCrash.log"
Write-Output "-Analyze `t Runs ndk-stack on the file after pulling"

exit
}

$global:currentDate = get-date
$global:recentDate = $Null
$global:recentTombstone = $Null

for ($i = 0; $i -lt 3; $i++) {
$stats = & adb shell stat /sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0$i
$date = (Select-String -Input $stats -Pattern "(?<=Modify: )\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?=.\d{9})").Matches.Value
if([string]::IsNullOrEmpty($date)) {
Write-Output "Failed to pull tombstone, exiting..."
exit 1;
}
$dateObj = [datetime]::ParseExact($date, "yyyy-MM-dd HH:mm:ss", $Null)
Comment on lines +26 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t abort the whole scan when one tombstone is unreadable.

Line 29 exits the script on the first parse miss. If one slot is missing/corrupt, you skip valid newer slots and fail the primary use case.

Suggested fix
+$foundAny = $false
 for ($i = 0; $i -lt 3; $i++) {
-    $stats = & adb shell stat /sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0$i
-    $date = (Select-String -Input $stats -Pattern "(?<=Modify: )\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?=.\d{9})").Matches.Value
-    if([string]::IsNullOrEmpty($date)) {
-        Write-Output "Failed to pull tombstone, exiting..."
-        exit 1;
+    $stats = & adb shell stat /sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0$i 2>$null
+    if ($LASTEXITCODE -ne 0) {
+        Write-Output "Skipping tombstone_0$i (not found)"
+        continue
+    }
+    $match = Select-String -InputObject $stats -Pattern 'Modify:\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})'
+    if ($match.Matches.Count -eq 0) {
+        Write-Output "Skipping tombstone_0$i (timestamp parse failed)"
+        continue
     }
+    $date = $match.Matches[0].Groups[1].Value
     $dateObj = [datetime]::ParseExact($date, "yyyy-MM-dd HH:mm:ss", $Null)
+    $foundAny = $true
     $difference = [math]::Round(($currentDate - $dateObj).TotalMinutes)
@@
 }
+
+if (-not $foundAny) {
+    Write-Error "No readable tombstones found."
+    exit 1
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/pull-tombstone.ps1` around lines 26 - 33, The loop that reads
tombstone_0$i currently calls exit 1 when $date is empty, aborting the whole
scan; change this to skip the unreadable slot instead: log a warning when the
regex parse fails for $stats (including the slot name tombstone_0$i), continue
the for loop rather than calling exit, and only fail the script afterward if
none of the slots produced a valid $dateObj (track a success flag or count).
Update references in this block ($stats, $date, $dateObj, tombstone_0$i) to
implement the non-fatal skip and final aggregate failure check.

$difference = [math]::Round(($currentDate - $dateObj).TotalMinutes)
if ($difference -eq 1) {
Write-Output "Found tombstone_0$i $difference minute ago"
} else {
Write-Output "Found tombstone_0$i $difference minutes ago"
}
if (-not $recentDate -or $recentDate -lt $dateObj) {
$recentDate = $dateObj
$recentTombstone = $i
}
}

Write-Output "Latest tombstone was tombstone_0$recentTombstone"

& adb pull /sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0$recentTombstone $fileName

if ($analyze) {
& $PSScriptRoot/ndk-stack.ps1 -logName:$fileName
}
Comment on lines +48 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate analysis on successful adb pull.

Line 51 can run even when Line 48 failed, which may analyze an old/local stale file and produce misleading output.

Suggested fix
 & adb pull /sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0$recentTombstone $fileName
+if ($LASTEXITCODE -ne 0) {
+    Write-Error "Failed to pull tombstone_0$recentTombstone"
+    exit 1
+}
 
 if ($analyze) {
     & $PSScriptRoot/ndk-stack.ps1 -logName:$fileName
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/pull-tombstone.ps1` around lines 48 - 52, The adb pull can fail but
$analyze block still runs; change the logic so ndk-stack.ps1 is only invoked
when the pull succeeded: run the pull (& adb pull /sdcard/... $fileName) and
capture its success (check $LASTEXITCODE or wrap the pull in a try/catch and set
a flag like $pullSucceeded), then make the conditional use both $analyze and
that success flag (e.g., if ($analyze -and $pullSucceeded) { &
$PSScriptRoot/ndk-stack.ps1 -logName:$fileName }) so you never analyze a
stale/local file.

2 changes: 2 additions & 0 deletions scripts/restart-game.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
adb shell am force-stop com.beatgames.beatsaber
adb shell am start com.beatgames.beatsaber/com.unity3d.player.UnityPlayerActivity
75 changes: 75 additions & 0 deletions scripts/start-logging.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
Param(
[Parameter(Mandatory=$false)]
[Switch] $self,

[Parameter(Mandatory=$false)]
[Switch] $all,

[Parameter(Mandatory=$false)]
[String] $custom="",

[Parameter(Mandatory=$false)]
[Switch] $file="",

[Parameter(Mandatory=$false)]
[Switch] $help,

[Parameter(Mandatory=$false)]
[Switch] $excludeHeader
)

if ($help -eq $true) {
if ($excludeHeader -eq $false) {
Write-Output "`"Start-Logging`" - Logs Beat Saber using `"adb logcat`""
Write-Output "`n-- Arguments --`n"
}

Write-Output "-Self `t`t Only Logs your mod and Crashes"
Write-Output "-All `t`t Logs everything, including logs made by the Quest itself"
Write-Output "-Custom `t Specify a specific logging pattern, e.g `"custom-types|questui`""
Write-Output "`t`t NOTE: The paterent `"AndriodRuntime|CRASH`" is always appended to a custom pattern"
Write-Output "-File `t`t Saves the output of the log to the file name given"

exit
}

$bspid = adb shell pidof com.beatgames.beatsaber
$command = "adb logcat "

# if ($all -eq $false) {
$loops = 0
while ([string]::IsNullOrEmpty($bspid) -and $loops -lt 6) {
Start-Sleep -Milliseconds 100
$bspid = adb shell pidof com.beatgames.beatsaber
$loops += 1
}

if ([string]::IsNullOrEmpty($bspid)) {
Write-Output "Could not connect to adb, exiting..."
exit 1
}

$command += "--pid $bspid"
# }
Comment on lines +39 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

-all is effectively ignored because --pid is always enforced.

Line 39–53 always resolve PID and append --pid, so Line 28 help text is inaccurate and full-device logging never happens. Gate PID logic behind if ($all -eq $false).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/start-logging.ps1` around lines 39 - 53, The script always resolves
and appends a PID causing the -all flag to be ignored; wrap the PID resolution
loop and the "$command += \"--pid $bspid\"" line inside a conditional that only
runs when $all -eq $false so full-device logging works when $all is true;
specifically gate the while loop that sets $bspid and the subsequent check/exit
and the $command += "--pid $bspid" behind the if ($all -eq $false) block so
$command is left unchanged when $all is true.


if ($all -eq $false) {
$pattern = "("
if ($self -eq $true) {
$pattern += "MultiplayerCore|"
}
if (![string]::IsNullOrEmpty($custom)) {
$pattern += "$custom|"
}
if ($pattern -eq "(") {
$pattern = "(QuestHook|modloader|"
}
$pattern += "AndroidRuntime|CRASH)"
$command += " | Select-String -pattern `"$pattern`""
}

if ($file -eq $true) {
$command += " | Out-File -FilePath $PSScriptRoot\..\log.log"
}

Write-Output "Logging using Command `"$command`""
Invoke-Expression $command
Comment on lines +67 to +75
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace Invoke-Expression command construction with direct invocation.

Line 67–75 executes interpolated strings containing $custom, which creates command-injection risk and is flagged by PSScriptAnalyzer.

Suggested fix
-Write-Output "Logging using Command `"$command`""
-Invoke-Expression $command
+Write-Output "Starting log capture..."
+if ($all) {
+    if ($file) { & adb logcat | Out-File -FilePath (Join-Path $PSScriptRoot "..\log.log") }
+    else { & adb logcat }
+} else {
+    if ($file) { & adb logcat --pid $bspid | Select-String -Pattern $pattern | Out-File -FilePath (Join-Path $PSScriptRoot "..\log.log") }
+    else { & adb logcat --pid $bspid | Select-String -Pattern $pattern }
+}
🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] 75-75: Invoke-Expression is used. Please remove Invoke-Expression from script and find other options instead.

(PSAvoidUsingInvokeExpression)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/start-logging.ps1` around lines 67 - 75, The current use of
Invoke-Expression to run the interpolated $command string (which may include
$custom) creates a command-injection risk; instead build the command and
arguments as a safe array and invoke directly (using the call operator & or
Start-Process -FilePath with -ArgumentList) rather than string interpolation and
Invoke-Expression. Locate where $command is assembled and replace the final
string concatenation/Invoke-Expression with a parameterized invocation that
passes the executable path and each argument separately, and handle piping
operations (Select-String/Out-File) by calling the appropriate cmdlets/functions
directly or by using Start-Process with -RedirectStandardOutput to the log file;
ensure $custom and other variables are passed as arguments, not embedded into a
single command string.

38 changes: 38 additions & 0 deletions scripts/validate-modjson.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
$mod = "./mod.json"

if (-not (Test-Path -Path $mod)) {
if (Test-Path -Path ".\mod.template.json") {
& qpm qmod build
Comment on lines +1 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the PowerShell script
cat -n scripts/validate-modjson.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1415


🏁 Script executed:

# Check repo structure and locations of the files mentioned
fd "mod\.json|mod\.template\.json|mod\.schema\.json" -t f

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 96


🏁 Script executed:

# Find where this script is invoked from
rg "validate-modjson" -t json -t yaml -t powershell -t shell

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 113


🏁 Script executed:

# Search for invocations of the validate script more broadly
rg "validate-modjson" --max-count=20

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 192


🏁 Script executed:

# Check package.json and other script files for invocations
cat -n package.json | head -100

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 123


🏁 Script executed:

# Check for any CI/CD files
fd "\.yml|\.yaml|\.json" scripts/ -t f

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 61


🏁 Script executed:

# Check repo structure to confirm file locations
ls -la | head -20

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1306


🏁 Script executed:

# Check where mod.template.json actually is
find . -name "mod.template.json" -o -name "mod.json"

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 98


🏁 Script executed:

# Look at the other scripts that call validate-modjson.ps1 to understand context
head -20 scripts/copy.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 457


🏁 Script executed:

# Also check createqmod.ps1
head -20 scripts/createqmod.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 496


Resolve paths from script's parent directory; add error handling for network requests.

Lines 1–5 rely on the current working directory for path resolution, which breaks when this script is invoked from a different directory. Resolve paths using Split-Path -Parent $PSScriptRoot (or $PSScriptRoot/..) to reference the repo root where mod.json and mod.template.json reside.

Additionally, line 21 downloads the schema with Invoke-WebRequest without error handling—add -ErrorAction Stop or wrap in a try-catch to avoid leaving partial or invalid state if the download fails. The same applies to Remove-Item on line 27.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/validate-modjson.ps1` around lines 1 - 5, The script currently uses
relative CWD paths for $mod and mod.template.json and performs network/file
operations without error handling; change the path resolution for $mod and the
template to use the script's directory (e.g., compute the repo root from
$PSScriptRoot via Split-Path -Parent and build the full paths with Join-Path) so
mod.json and mod.template.json are found regardless of where the script is
invoked, and add robust error handling around Invoke-WebRequest and Remove-Item
(use -ErrorAction Stop or wrap each call in try/catch) to surface
download/remove errors and clean up any partial files if the download fails.

if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
else {
Write-Output "Error: mod.json and mod.template.json were not present"
exit 1
}
}

Write-Output "Creating qmod from mod.json"

$psVersion = $PSVersionTable.PSVersion.Major
if ($psVersion -ge 6) {
$schemaUrl = "https://raw.githubusercontent.com/Lauriethefish/QuestPatcher.QMod/main/QuestPatcher.QMod/Resources/qmod.schema.json"
Invoke-WebRequest $schemaUrl -OutFile ./mod.schema.json

$schema = "./mod.schema.json"
$modJsonRaw = Get-Content $mod -Raw
$modSchemaRaw = Get-Content $schema -Raw

Remove-Item $schema

Write-Output "Validating mod.json..."
if (-not ($modJsonRaw | Test-Json -Schema $modSchemaRaw)) {
Write-Output "Error: mod.json is not valid"
exit 1
}
Comment on lines +21 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/validate-modjson.ps1

Repository: EnderdracheLP/MultiplayerCore.Quest

Length of output: 1415


Add error handling around schema download and validation cleanup.

If Invoke-WebRequest fails at line 21, subsequent Get-Content/Remove-Item calls can fail noisily or silently. Wrap download/validation in try/finally with -ErrorAction Stop to ensure cleanup occurs and errors are clear.

Suggested fix
- Invoke-WebRequest $schemaUrl -OutFile ./mod.schema.json
-
- $schema = "./mod.schema.json"
- $modJsonRaw = Get-Content $mod -Raw
- $modSchemaRaw = Get-Content $schema -Raw
-
- Remove-Item $schema
-
- Write-Output "Validating mod.json..."
- if (-not ($modJsonRaw | Test-Json -Schema $modSchemaRaw)) {
-     Write-Output "Error: mod.json is not valid"
-     exit 1
- }
+ $schema = "./mod.schema.json"
+ try {
+     Invoke-WebRequest $schemaUrl -OutFile $schema -ErrorAction Stop
+     $modJsonRaw = Get-Content $mod -Raw -ErrorAction Stop
+     $modSchemaRaw = Get-Content $schema -Raw -ErrorAction Stop
+     Write-Output "Validating mod.json..."
+     if (-not ($modJsonRaw | Test-Json -Schema $modSchemaRaw)) {
+         Write-Output "Error: mod.json is not valid"
+         exit 1
+     }
+ } finally {
+     if (Test-Path $schema) { Remove-Item $schema -Force }
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Invoke-WebRequest $schemaUrl -OutFile ./mod.schema.json
$schema = "./mod.schema.json"
$modJsonRaw = Get-Content $mod -Raw
$modSchemaRaw = Get-Content $schema -Raw
Remove-Item $schema
Write-Output "Validating mod.json..."
if (-not ($modJsonRaw | Test-Json -Schema $modSchemaRaw)) {
Write-Output "Error: mod.json is not valid"
exit 1
}
$schema = "./mod.schema.json"
try {
Invoke-WebRequest $schemaUrl -OutFile $schema -ErrorAction Stop
$modJsonRaw = Get-Content $mod -Raw -ErrorAction Stop
$modSchemaRaw = Get-Content $schema -Raw -ErrorAction Stop
Write-Output "Validating mod.json..."
if (-not ($modJsonRaw | Test-Json -Schema $modSchemaRaw)) {
Write-Output "Error: mod.json is not valid"
exit 1
}
} finally {
if (Test-Path $schema) { Remove-Item $schema -Force }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/validate-modjson.ps1` around lines 21 - 33, Wrap the download and
validation sequence in a try/catch/finally: call Invoke-WebRequest with
-ErrorAction Stop (and likewise use -ErrorAction Stop on Get-Content and
Test-Json) inside the try block, perform validation using $modJsonRaw |
Test-Json -Schema $modSchemaRaw in the try, log and exit with non-zero in the
catch when any step fails, and always call Remove-Item $schema in the finally to
ensure cleanup; reference the existing symbols Invoke-WebRequest, $schema,
$modJsonRaw, $modSchemaRaw, Test-Json, Remove-Item to locate where to add
try/catch/finally and error handling.

}
else {
Write-Output "Could not validate mod.json with schema: powershell version was too low (< 6)"
}
exit
14 changes: 7 additions & 7 deletions src/Beatmaps/Abstractions/DifficultyColors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ namespace MultiplayerCore::Beatmaps::Abstractions {

void DifficultyColors::Deserialize(LiteNetLib::Utils::NetDataReader* reader) {
uint8_t colors = reader->GetByte();
if (((colors << 0) & 0x1) != 0) colorLeft = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors << 1) & 0x1) != 0) colorRight = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors << 2) & 0x1) != 0) envColorLeft = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors << 3) & 0x1) != 0) envColorRight = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors << 4) & 0x1) != 0) envColorLeftBoost = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors << 5) & 0x1) != 0) envColorRightBoost = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors << 6) & 0x1) != 0) obstacleColor = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 0) & 0x1) != 0) colorLeft = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 1) & 0x1) != 0) colorRight = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 2) & 0x1) != 0) envColorLeft = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 3) & 0x1) != 0) envColorRight = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 4) & 0x1) != 0) envColorLeftBoost = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 5) & 0x1) != 0) envColorRightBoost = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
if (((colors >> 6) & 0x1) != 0) obstacleColor = MultiplayerCore::Utils::ExtraSongData::MapColor(reader);
}
}
Loading
Loading