Compare commits
37
Commits
c4e34bcde5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d9e04c09d | ||
|
|
08246ddb01
|
||
|
|
f92105c97d | ||
|
|
ddd55c0c58 | ||
|
|
2760dee6a7 | ||
|
|
bb12b8d02d | ||
|
|
6c90f9dc08
|
||
|
|
7debc0240c
|
||
|
|
e9d8e93427
|
||
|
|
2695dcdbba
|
||
|
|
88ba3cdc48
|
||
|
|
60cf457230
|
||
|
|
b750ce7545
|
||
|
|
34e15e80e2
|
||
|
|
de720ce33c | ||
|
|
64248f5d4d | ||
|
|
0ba5c52e10 | ||
|
|
9b8c528d23 | ||
|
|
0bb94951b2 | ||
|
|
2a513cbd5b | ||
|
|
3f46703d7a | ||
|
|
d59b92b4dd | ||
|
|
9292d01f6a | ||
|
|
f5f9cd76ec | ||
|
|
4a21bd75e3 | ||
|
|
bbce96426a | ||
|
|
7c0445606a | ||
|
|
a73773a0fb | ||
|
|
efc9a2aca9 | ||
|
|
4d9b34dc36 | ||
|
|
f37aedf79f | ||
|
|
178f7ae924 | ||
|
|
add2af8ec1 | ||
|
|
ce4517ed0c | ||
|
|
0dfdcca9ba | ||
|
|
d1f0f44bd6 | ||
|
|
9c17781bbf |
@@ -0,0 +1,105 @@
|
||||
name: Verify ShrinkSDK Package Hosts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
catalog:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker.m.daocloud.io/library/node:22-bookworm
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate submodules and version catalog
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ref="${{ gitea.sha }}"
|
||||
git init .
|
||||
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
|
||||
git fetch --depth=1 origin "$ref"
|
||||
git checkout --detach FETCH_HEAD
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive
|
||||
bash Tools/CI/Validate-Submodules.sh "$PWD"
|
||||
node Tools/Release/update-shrinksdk-versions.mjs --check
|
||||
|
||||
editmode:
|
||||
needs: catalog
|
||||
runs-on: unity-2022.3.62f3
|
||||
container:
|
||||
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
|
||||
volumes:
|
||||
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
|
||||
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
UNITY_VERSION: 2022.3.62f3
|
||||
steps:
|
||||
- name: Fetch exact Workspace revision and submodules
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ref="${{ gitea.sha }}"
|
||||
git init .
|
||||
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
|
||||
git fetch --depth=1 origin "$ref"
|
||||
git checkout --detach FETCH_HEAD
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive
|
||||
bash Tools/CI/Validate-Submodules.sh "$PWD"
|
||||
|
||||
- name: Run every standalone package EditMode host
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
machine_id_file="/root/.local/share/unity3d/Unity/.machine-id"
|
||||
if test -s "$machine_id_file"; then
|
||||
cat "$machine_id_file" > /etc/machine-id
|
||||
echo "Unity machine identity restored"
|
||||
fi
|
||||
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
|
||||
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
|
||||
test -n "$unity_bin"
|
||||
"$unity_bin" -version | head -n 1 | grep -F "$UNITY_VERSION"
|
||||
|
||||
failed=0
|
||||
workspace_root="$(pwd)"
|
||||
for relative_host in Assets/Modules/*/Development~/UnityProject; do
|
||||
host="$workspace_root/$relative_host"
|
||||
package_root="$(dirname "$(dirname "$host")")"
|
||||
package_name="$(basename "$package_root")"
|
||||
results_dir="$host/TestResults"
|
||||
results_file="$results_dir/editmode.xml"
|
||||
log_file="$results_dir/unity.log"
|
||||
mkdir -p "$results_dir"
|
||||
echo "::group::${package_name} EditMode"
|
||||
status=0
|
||||
"$unity_bin" \
|
||||
-batchmode \
|
||||
-nographics \
|
||||
-projectPath "$host" \
|
||||
-runTests \
|
||||
-testPlatform EditMode \
|
||||
-testResults "$results_file" \
|
||||
-logFile "$log_file" || status=$?
|
||||
if test "$status" -ne 0 || ! test -s "$results_file" || ! grep -q 'result="Passed"' "$results_file"; then
|
||||
failed=1
|
||||
echo "${package_name} EditMode verification failed (Unity exit status: ${status})" >&2
|
||||
if test -s "$results_file"; then
|
||||
echo "${package_name} test result summary:" >&2
|
||||
sed -n '1,40p' "$results_file" >&2
|
||||
else
|
||||
echo "${package_name} did not produce a test result file." >&2
|
||||
fi
|
||||
echo "${package_name} relevant Unity diagnostics:" >&2
|
||||
grep -in -E 'error CS|compilation failed|test.*(failed|failure)|exception|fatal|no tests were executed|failed to' "$log_file" \
|
||||
| tail -n 160 >&2 || true
|
||||
else
|
||||
echo "${package_name} EditMode verification passed"
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
exit "$failed"
|
||||
@@ -0,0 +1,144 @@
|
||||
name: Publish inspection and optional packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
NUGET_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.SHRINKSDK_PACKAGE_TOKEN }}
|
||||
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: '1'
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: '1'
|
||||
LD_LIBRARY_PATH: /opt/dotnet-libs/usr/lib/x86_64-linux-gnu
|
||||
SSL_CERT_FILE: /opt/ca-certificates.crt
|
||||
steps:
|
||||
- name: Fetch exact workspace and required submodules
|
||||
env:
|
||||
RELEASE_SHA: ${{ gitea.sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'NODE'
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
const base = 'https://git.crash.work';
|
||||
const sha = process.env.RELEASE_SHA;
|
||||
if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error('An exact commit is required');
|
||||
async function fetchSource(repo, revision, destination) {
|
||||
const response = await fetch(`${base}/ShrinkSDK/${repo}/archive/${revision}.tar.gz`);
|
||||
if (!response.ok) throw new Error(`Cannot fetch ${repo}: ${response.status}`);
|
||||
await writeFile('source.tar.gz', new Uint8Array(await response.arrayBuffer()));
|
||||
await mkdir(destination, { recursive: true });
|
||||
execFileSync('tar', ['-xzf', 'source.tar.gz', '--strip-components=1', '-C', destination]);
|
||||
}
|
||||
await fetchSource('Workspace', sha, 'release');
|
||||
const response = await fetch(`${base}/api/v1/repos/ShrinkSDK/Workspace/git/trees/${sha}?recursive=true`);
|
||||
if (!response.ok) throw new Error(`Cannot resolve submodules: ${response.status}`);
|
||||
const tree = await response.json();
|
||||
for (const repo of ['ShrinkShared.CodeGen', 'ShrinkNetwork']) {
|
||||
const path = `Assets/Modules/${repo}`;
|
||||
const entry = tree.tree.find(item => item.path === path && item.type === 'commit');
|
||||
if (!entry) throw new Error(`Missing pinned submodule: ${path}`);
|
||||
await fetchSource(repo, entry.sha, `release/${path}`);
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Install .NET 8 SDK
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'NODE'
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { rootCertificates } from 'node:tls';
|
||||
|
||||
await writeFile('/opt/ca-certificates.crt', rootCertificates.join('\n'));
|
||||
|
||||
const metadataResponse = await fetch('https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/8.0/releases.json');
|
||||
if (!metadataResponse.ok) throw new Error(`Release metadata download failed: ${metadataResponse.status}`);
|
||||
const metadata = await metadataResponse.json();
|
||||
const sdkVersion = metadata['latest-sdk'];
|
||||
const release = metadata.releases.find(item => item.sdk?.version === sdkVersion);
|
||||
const file = release?.sdk?.files?.find(item => item.rid === 'linux-x64' && item.name.endsWith('.tar.gz'));
|
||||
if (!file) throw new Error(`Linux x64 SDK archive not found for ${sdkVersion}`);
|
||||
const archiveResponse = await fetch(file.url);
|
||||
if (!archiveResponse.ok) throw new Error(`SDK download failed: ${archiveResponse.status}`);
|
||||
await writeFile('/tmp/dotnet-sdk.tar.gz', new Uint8Array(await archiveResponse.arrayBuffer()));
|
||||
|
||||
const poolUrl = 'https://deb.debian.org/debian-security/pool/updates/main/o/openssl/';
|
||||
const poolResponse = await fetch(poolUrl);
|
||||
if (!poolResponse.ok) throw new Error(`OpenSSL package index download failed: ${poolResponse.status}`);
|
||||
const poolIndex = await poolResponse.text();
|
||||
const packages = [...poolIndex.matchAll(/href="(libssl3_[^"]+_amd64\.deb)"/g)].map(match => match[1]).sort();
|
||||
const packageName = packages.at(-1);
|
||||
if (!packageName) throw new Error('Debian libssl3 package was not found');
|
||||
const packageResponse = await fetch(poolUrl + packageName);
|
||||
if (!packageResponse.ok) throw new Error(`OpenSSL package download failed: ${packageResponse.status}`);
|
||||
await writeFile('/tmp/libssl3.deb', new Uint8Array(await packageResponse.arrayBuffer()));
|
||||
NODE
|
||||
mkdir -p /opt/dotnet
|
||||
tar -xzf /tmp/dotnet-sdk.tar.gz -C /opt/dotnet
|
||||
mkdir -p /opt/dotnet-libs
|
||||
dpkg-deb -x /tmp/libssl3.deb /opt/dotnet-libs
|
||||
rm -f /tmp/dotnet-sdk.tar.gz
|
||||
rm -f /tmp/libssl3.deb
|
||||
/opt/dotnet/dotnet --info
|
||||
|
||||
- name: Build supplemental NuGet packages
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="/opt/dotnet:$PATH"
|
||||
cd release
|
||||
projects=(
|
||||
Assets/ShrinkInspection/DotNet~/ShrinkSDK.Inspection.csproj
|
||||
Assets/Modules/ShrinkNetwork/Adapters~/MessagePack/ShrinkSDK.Network.MessagePack.csproj
|
||||
Tools/AgentSupport/Shrink.Inspect/Shrink.Inspect.csproj
|
||||
)
|
||||
for project in "${projects[@]}"; do
|
||||
dotnet restore "$project" --configfile Assets/Modules/ShrinkNetwork/NuGet.Config
|
||||
dotnet pack "$project" -c Release --no-restore -o "$PWD/packages"
|
||||
done
|
||||
|
||||
- name: Publish NuGet and UPM packages
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: "${NUGET_AUTH_TOKEN:?SHRINKSDK_PACKAGE_TOKEN is required}"
|
||||
export PATH="/opt/dotnet:$PATH"
|
||||
cd release
|
||||
dotnet nuget push 'packages/*.nupkg' --api-key "$NUGET_AUTH_TOKEN" --source https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json --skip-duplicate
|
||||
npmrc="$HOME/.npmrc"
|
||||
trap 'rm -f "$npmrc"' EXIT
|
||||
printf '%s\n' \
|
||||
'registry=https://git.crash.work/api/packages/ShrinkSDK/npm/' \
|
||||
'//git.crash.work/api/packages/ShrinkSDK/npm/:_authToken=${NODE_AUTH_TOKEN}' > "$npmrc"
|
||||
for package in Assets/ShrinkInspection Assets/Modules/ShrinkNetwork/Adapters~/MessagePack; do
|
||||
# Stage only Unity source and documentation; dotnet build output is not a UPM asset.
|
||||
stage="$PWD/upm-$(basename "$package")"
|
||||
mkdir "$stage"
|
||||
cp "$package/package.json" "$package/README.md" "$stage/"
|
||||
cp -r "$package/Runtime" "$stage/"
|
||||
if [[ -d "$package/Editor" ]]; then cp -r "$package/Editor" "$stage/"; fi
|
||||
find "$package" -maxdepth 1 -name '*.meta' -exec cp '{}' "$stage/" \;
|
||||
name="$(node -p "require('./$package/package.json').name")"
|
||||
version="$(node -p "require('./$package/package.json').version")"
|
||||
export PACKAGE_NAME="$name" PACKAGE_VERSION="$version"
|
||||
exists="$(node --input-type=module <<'NODE'
|
||||
const url = `https://git.crash.work/api/packages/ShrinkSDK/npm/${encodeURIComponent(process.env.PACKAGE_NAME)}`;
|
||||
const response = await fetch(url);
|
||||
if (response.status === 404) console.log('no');
|
||||
else {
|
||||
if (!response.ok) throw new Error(`Registry lookup failed: ${response.status}`);
|
||||
const metadata = await response.json();
|
||||
console.log(metadata.versions?.[process.env.PACKAGE_VERSION] ? 'yes' : 'no');
|
||||
}
|
||||
NODE
|
||||
)"
|
||||
if [[ "$exists" = yes ]]; then echo "Already published: $name@$version"; continue; fi
|
||||
(cd "$stage" && npm publish --registry=https://git.crash.work/api/packages/ShrinkSDK/npm/)
|
||||
done
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Verify Published UPM Consumers
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
consumers:
|
||||
runs-on: unity-2022.3.62f3
|
||||
container:
|
||||
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
|
||||
volumes:
|
||||
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-license:/root/.local/share/unity3d/Unity
|
||||
- /www/dk_project/bt-recovery/gitea/runner/seedskey-unity-entitlements:/root/.config/unity3d/Unity/licenses
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Fetch exact Workspace revision
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ref="${{ gitea.sha }}"
|
||||
git init .
|
||||
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
|
||||
git fetch --depth=1 origin "$ref"
|
||||
git checkout --detach FETCH_HEAD
|
||||
|
||||
- name: Verify registry, Git tag, and installer consumers
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
machine_id_file="/root/.local/share/unity3d/Unity/.machine-id"
|
||||
if test -s "$machine_id_file"; then
|
||||
cat "$machine_id_file" > /etc/machine-id
|
||||
echo "Unity machine identity restored"
|
||||
fi
|
||||
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
|
||||
bash Tools/UpmConsumerValidation/Validate-PublishedUpmConsumers.sh "$PWD"
|
||||
@@ -7,7 +7,29 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
catalog:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker.m.daocloud.io/library/node:22-bookworm
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate submodules and version catalog
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ref="${{ gitea.sha }}"
|
||||
git init .
|
||||
git remote add origin "https://git.crash.work/ShrinkSDK/Workspace.git"
|
||||
git fetch --depth=1 origin "$ref"
|
||||
git checkout --detach FETCH_HEAD
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive
|
||||
bash Tools/CI/Validate-Submodules.sh "$PWD"
|
||||
node Tools/Release/update-shrinksdk-versions.mjs --check
|
||||
|
||||
unity:
|
||||
needs: catalog
|
||||
runs-on: unity-2022.3.62f3
|
||||
container:
|
||||
image: docker.1panel.live/unityci/editor:ubuntu-2022.3.62f3-windows-mono-3
|
||||
@@ -30,10 +52,7 @@ jobs:
|
||||
git checkout --detach FETCH_HEAD
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive
|
||||
module_count="$(git submodule status --recursive | wc -l | tr -d ' ')"
|
||||
test "$module_count" = "21"
|
||||
test -z "$(git submodule status --recursive | grep '^-')"
|
||||
git submodule status --recursive
|
||||
bash Tools/CI/Validate-Submodules.sh "$PWD"
|
||||
|
||||
- name: Compile Workspace and validate package graph
|
||||
shell: bash
|
||||
@@ -54,6 +73,7 @@ jobs:
|
||||
cat "$machine_id_file" > /etc/machine-id
|
||||
echo "Unity machine identity restored"
|
||||
fi
|
||||
git config --global url."https://ghfast.top/https://github.com/".insteadOf "https://github.com/"
|
||||
unity_bin="$(command -v unity-editor || command -v unity || command -v Unity || true)"
|
||||
test -n "$unity_bin"
|
||||
mkdir -p Artifacts
|
||||
@@ -66,5 +86,7 @@ jobs:
|
||||
-projectPath "$PWD" \
|
||||
-executeMethod ShrinkSDK.WorkspaceValidation.ShrinkSdkWorkspaceValidation.Run \
|
||||
-logFile "$PWD/Artifacts/unity-workspace-validation.log" || status=$?
|
||||
echo '--- Unity workspace errors and exceptions ---'
|
||||
grep -in -C 12 -E 'exception|buildfailed|build failed|error|failed|compilererror|invalidoperation|nullreference|package manager' "$PWD/Artifacts/unity-workspace-validation.log" | tail -n 260 || true
|
||||
tail -n 240 "$PWD/Artifacts/unity-workspace-validation.log" || true
|
||||
exit "$status"
|
||||
|
||||
+11
@@ -45,6 +45,9 @@ ExportedObj/
|
||||
*.unityproj
|
||||
*.sln
|
||||
*.csproj
|
||||
!Tools/**/*.csproj
|
||||
Tools/**/bin/
|
||||
Tools/**/obj/
|
||||
*.suo
|
||||
*.tmp
|
||||
*.user
|
||||
@@ -79,6 +82,8 @@ mono_crash.*
|
||||
# namespace-suffixed Unity Integration.App packages in source control.
|
||||
!Assets/Modules/*.Integration.App/
|
||||
!Assets/Modules/*.Integration.App/**
|
||||
!Godot/Packages/*.Integration.App/
|
||||
!Godot/Packages/*.Integration.App/**
|
||||
|
||||
# Crashlytics generated file
|
||||
crashlytics-build.properties
|
||||
@@ -143,3 +148,9 @@ GeneratedServers/ExampleGreedMod.Rules/*
|
||||
!GeneratedServers/ExampleGreedMod.Rules/ExampleGreedMod.Rules.csproj
|
||||
!GeneratedServers/ExampleGreedMod.Rules/pack-mod.ps1
|
||||
GeneratedModSdk/
|
||||
.mimosa/
|
||||
|
||||
!Assets/ShrinkInspection/DotNet~/*.csproj
|
||||
|
||||
Assets/ShrinkInspection/DotNet~/bin/
|
||||
Assets/ShrinkInspection/DotNet~/obj/
|
||||
|
||||
@@ -82,3 +82,11 @@
|
||||
path = Assets/Modules/ShrinkInstaller
|
||||
url = https://git.crash.work/ShrinkSDK/Installer.git
|
||||
branch = main
|
||||
[submodule "Assets/Modules/ShrinkRuntime.Abstractions"]
|
||||
path = Assets/Modules/ShrinkRuntime.Abstractions
|
||||
url = https://git.crash.work/ShrinkSDK/ShrinkRuntime.Abstractions.git
|
||||
branch = main
|
||||
[submodule "Godot/Packages/ShrinkSDK.Godot"]
|
||||
path = Godot/Packages/ShrinkSDK.Godot
|
||||
url = https://git.crash.work/ShrinkSDK/ShrinkGodot.git
|
||||
branch = main
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2319a25c4ac4b0941824ffb1fbfadcf4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 908affa5f1a95214fa21a165799e7e56
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.TestTools.TestRunner.Api;
|
||||
using UnityEngine;
|
||||
[InitializeOnLoad]
|
||||
public static class PlayerAcceptance
|
||||
{
|
||||
private static readonly TestRunnerApi Api;
|
||||
static PlayerAcceptance() { Api = ScriptableObject.CreateInstance<TestRunnerApi>(); Api.RegisterCallbacks(new Results()); }
|
||||
public static string Run()
|
||||
{
|
||||
SessionState.SetBool("Shrink.PlayerAcceptance.Active", true);
|
||||
return Api.Execute(new ExecutionSettings(new Filter { testMode = TestMode.PlayMode, assemblyNames = new[] { "ShrinkSDK.AgentSupport.PlayerTests" }, targetPlatform = BuildTarget.StandaloneWindows64 }));
|
||||
}
|
||||
private sealed class Results : ICallbacks
|
||||
{
|
||||
public void RunStarted(ITestAdaptor tests) { }
|
||||
public void TestStarted(ITestAdaptor test) { }
|
||||
public void TestFinished(ITestResultAdaptor result) { }
|
||||
public void RunFinished(ITestResultAdaptor result)
|
||||
{
|
||||
if (!SessionState.GetBool("Shrink.PlayerAcceptance.Active", false)) return;
|
||||
SessionState.SetBool("Shrink.PlayerAcceptance.Active", false);
|
||||
Directory.CreateDirectory("Artifacts/AgentSupport");
|
||||
File.WriteAllText("Artifacts/AgentSupport/unity-player-results.xml", result.ToXml().OuterXml);
|
||||
Debug.Log("Shrink Player acceptance: " + result.PassCount + " passed, " + result.FailCount + " failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84cf42c70f18cd84b9cdca62969b6af8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "ShrinkSDK.AgentSupport.EditorTests",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"optionalUnityReferences": [
|
||||
"TestAssemblies"
|
||||
],
|
||||
"autoReferenced": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7d77cac6e21f93479774330dd7bda65
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 566d8dd60d1f29a42857bf011769527c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkContext;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
public sealed class PlayerContracts
|
||||
{
|
||||
[Test]
|
||||
public void DotNetGoldenFrameIsIdenticalInPlayer()
|
||||
{
|
||||
var bytes = UnityEngine.Resources.Load<UnityEngine.TextAsset>("ShrinkV2Golden").bytes;
|
||||
var packet = ShrinkPacketCodec.Decode(bytes);
|
||||
var codec = new ShrinkJsonNetworkSerializer();
|
||||
var message = (Message)codec.Deserialize(packet.Payload, typeof(Message));
|
||||
Assert.That(message.Text, Is.EqualTo("跨宿主-v2"));
|
||||
using var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, message);
|
||||
Assert.That(encoded.WrittenMemory.ToArray(), Is.EqualTo(bytes));
|
||||
}
|
||||
[Test]
|
||||
public void BinaryEnvelopeAndLegacyRejection()
|
||||
{
|
||||
var codec = new ShrinkJsonNetworkSerializer();
|
||||
foreach (var size in new[] { 64, 1024, 32768 })
|
||||
{
|
||||
var message = new Message { Text = new string('x', size) };
|
||||
using var frame = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, message);
|
||||
var packet = ShrinkPacketCodec.Decode(frame.WrittenMemory);
|
||||
Assert.That(((Message)codec.Deserialize(packet.Payload, typeof(Message))).Text, Is.EqualTo(message.Text));
|
||||
var copy = frame.WrittenMemory.ToArray(); copy[4] = 1;
|
||||
Assert.Throws<ShrinkProtocolException>(() => ShrinkPacketCodec.Decode(copy));
|
||||
}
|
||||
}
|
||||
[UnityTest]
|
||||
public IEnumerator ContextTenThousandRetirements() => UniTask.ToCoroutine(async () =>
|
||||
{
|
||||
var runtime = new ShrinkContextRuntime(16);
|
||||
var watch = Stopwatch.StartNew();
|
||||
for (var i = 0; i < 10000; i++) await runtime.RetireAsync(runtime.Use(new Provider()));
|
||||
Assert.That(runtime.Fibers.Count, Is.Zero);
|
||||
Assert.That(runtime.CaptureDiagnostic().Fibers.Count, Is.EqualTo(16));
|
||||
UnityEngine.Debug.Log("PLAYER Context 10000 retirements ms=" + watch.Elapsed.TotalMilliseconds);
|
||||
await runtime.ShutdownAsync();
|
||||
});
|
||||
[Test]
|
||||
public void EventDispatchAllocations()
|
||||
{
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<Event>(ShrinkGeneratedBinding.RuntimeResolver, null, null, null, _ => { }, ShrinkEventPriority.Normal, 0, false);
|
||||
for (var i = 0; i < 100; i++) EventBus.Post(new Event());
|
||||
var watch = Stopwatch.StartNew();
|
||||
for (var i = 0; i < 10000; i++) EventBus.Post(new Event());
|
||||
watch.Stop();
|
||||
var events=PlayerPerformance.AllocationEvents(() => EventBus.Post(new Event()),10000);
|
||||
UnityEngine.Debug.Log("PLAYER EventBus 10000 posts allocationEvents="+events+" ms="+watch.Elapsed.TotalMilliseconds);
|
||||
using var host=new ShrinkEventBusHost();
|
||||
var local=host.CreateBus(ShrinkBusKey.Game,ShrinkBusOptions.Inline());
|
||||
using var localBinding=ShrinkGeneratedBinding.Subscribe<Event>(host,null,null,null,_=>{},ShrinkEventPriority.Normal,0,false);
|
||||
for(var i=0;i<100;i++) local.Post(new Event());
|
||||
var isolatedEvents=PlayerPerformance.AllocationEvents(()=>local.Post(new Event()),10000);
|
||||
UnityEngine.Debug.Log("PLAYER EventBus isolated 10000 posts allocationEvents="+isolatedEvents);
|
||||
Assert.That(isolatedEvents,Is.Zero);
|
||||
Assert.Throws<ArgumentNullException>(()=>local.Post<ReferenceEvent>(null));
|
||||
}
|
||||
[UnityTest]
|
||||
public IEnumerator LoopbackRpc() => UniTask.ToCoroutine(async () =>
|
||||
{
|
||||
var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b);
|
||||
var client = new ShrinkNetworkService(); var server = new ShrinkNetworkService();
|
||||
foreach (var service in new[] { client, server }) { service.RegisterMessage<Request>(1); service.RegisterMessage<Response>(2); }
|
||||
server.RegisterRequestHandler<Request, Response>((_, r) => UniTask.FromResult(new Response { Text = r.Text }));
|
||||
client.BindTransport(a); server.BindTransport(b); a.OpenSession(1);
|
||||
try { Assert.That((await client.CallAsync<Request, Response>(client.Sessions[1], new Request { Text = "Unity Player" })).Text, Is.EqualTo("Unity Player")); }
|
||||
finally { a.Stop(); b.Stop(); }
|
||||
});
|
||||
[UnityTest]
|
||||
public IEnumerator SavePhaseMatrix() => UniTask.ToCoroutine(async () =>
|
||||
{
|
||||
var root = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ShrinkPlayerSave-" + Guid.NewGuid().ToString("N"));
|
||||
var storage = new ShrinkDataSaver.LocalStorageProvider(root);
|
||||
var initialize = typeof(ShrinkDataSaver.ShrinkSave).GetMethod("Initialize", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
initialize.Invoke(null, new object[] { storage, System.IO.Path.Combine(root, "saves"), ".sav", 1 });
|
||||
try
|
||||
{
|
||||
foreach (var size in new[] { 100 * 1024, 1024 * 1024, 10 * 1024 * 1024 })
|
||||
foreach (var background in new[] { false, true })
|
||||
{
|
||||
var value = new string('s', size); var owner = System.Threading.Thread.CurrentThread.ManagedThreadId; var captureThread = -1;
|
||||
ShrinkDataSaver.ShrinkSave.RegisterModule("player-benchmark", () => { captureThread = System.Threading.Thread.CurrentThread.ManagedThreadId; return value; }, _ => { });
|
||||
var options = new ShrinkDataSaver.SaveOptions { EncodeInBackground = background, CollectTimings = true };
|
||||
await ShrinkDataSaver.ShrinkSave.SaveSlotAsync(0, options);
|
||||
Assert.That(captureThread, Is.EqualTo(owner));
|
||||
Assert.That(options.LastTimings.EncodedBytes, Is.GreaterThan(size));
|
||||
UnityEngine.Debug.Log("PLAYER Save " + size + " background=" + background + " " + "captureMs=" + options.LastTimings.CaptureMilliseconds + " encodeMs=" + options.LastTimings.EncodeMilliseconds + " encryptMs=" + options.LastTimings.EncryptMilliseconds + " writeMs=" + options.LastTimings.WriteMilliseconds + " bytes=" + options.LastTimings.EncodedBytes);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ShrinkDataSaver.ShrinkSave.UnregisterModule("player-benchmark");
|
||||
if (System.IO.Directory.Exists(root)) System.IO.Directory.Delete(root, true);
|
||||
}
|
||||
});
|
||||
public sealed class Message : IShrinkNetworkMessage { public string Text { get; set; } }
|
||||
public sealed class Request : IShrinkNetworkRequest { public string Text { get; set; } }
|
||||
public sealed class Response : ShrinkRpcResponseBase { public string Text { get; set; } }
|
||||
public readonly struct Event : IShrinkEvent { }
|
||||
public sealed class ReferenceEvent : IShrinkEvent { }
|
||||
private sealed class Provider : IShrinkComponent
|
||||
{
|
||||
public string Name => "test";
|
||||
public IReadOnlyList<string> Inject => Array.Empty<string>();
|
||||
public IReadOnlyList<string> Provide => new[] { "value" };
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object config) { ctx.Set("value", this); return UniTask.CompletedTask; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 531de603668484545b5993c8f4894027
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
using UnityEngine.TestTools;
|
||||
using Unity.Profiling;
|
||||
|
||||
public sealed class PlayerPerformance
|
||||
{
|
||||
private static byte[] _allocationProbe;
|
||||
public static long AllocationEvents(Action action, int count)
|
||||
{
|
||||
using var recorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC.Alloc", 65536,
|
||||
ProfilerRecorderOptions.StartImmediately | ProfilerRecorderOptions.CollectOnlyOnCurrentThread);
|
||||
if (!recorder.Valid) throw new InvalidOperationException("GC.Alloc recorder unavailable.");
|
||||
for (var i=0;i<count;i++) action();
|
||||
recorder.Stop();
|
||||
if (recorder.Count == recorder.Capacity) throw new InvalidOperationException("GC.Alloc recorder saturated.");
|
||||
return recorder.Count;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllocationCounterDetectsKnownAllocation()
|
||||
{
|
||||
var before=GC.GetAllocatedBytesForCurrentThread();
|
||||
_allocationProbe=new byte[1024];
|
||||
var delta=GC.GetAllocatedBytesForCurrentThread()-before;
|
||||
var events=AllocationEvents(() => _allocationProbe=new byte[1024],10);
|
||||
GC.KeepAlive(_allocationProbe);
|
||||
Assert.That(events,Is.GreaterThanOrEqualTo(10));
|
||||
UnityEngine.Debug.Log("PLAYER allocation calibration managedCounterDelta="+delta+" profilerEvents="+events+"; use profiler events, not uncalibrated bytes.");
|
||||
}
|
||||
|
||||
private static void Measure(string scenario, int count, Action action)
|
||||
{
|
||||
for (var i = 0; i < Math.Min(count, 20); i++) action();
|
||||
var latency = new double[count];
|
||||
var started = Stopwatch.GetTimestamp();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var tick = Stopwatch.GetTimestamp(); action();
|
||||
latency[i] = (Stopwatch.GetTimestamp() - tick) * 1e6 / Stopwatch.Frequency;
|
||||
}
|
||||
var elapsed = (Stopwatch.GetTimestamp() - started) / (double)Stopwatch.Frequency;
|
||||
Array.Sort(latency);
|
||||
var allocationSamples=Math.Min(count,10);
|
||||
var allocationEvents=AllocationEvents(action,allocationSamples);
|
||||
UnityEngine.Debug.Log(FormattableString.Invariant($"PLAYER PERF {scenario} n={count} ops={count/elapsed:F2} allocBytes=unsupported allocationSamples={allocationSamples} allocEventsPerOp={allocationEvents/(double)allocationSamples:F2} p50us={latency[(int)(count*.5)]:F2} p95us={latency[(int)(count*.95)]:F2} p99us={latency[(int)(count*.99)]:F2}"));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator NetworkMatrix() => UniTask.ToCoroutine(async () =>
|
||||
{
|
||||
foreach (var bytes in new[] {64, 1024, 32768})
|
||||
foreach (var sessions in new[] {1, 32, 256})
|
||||
{
|
||||
var client = new ShrinkNetworkService(); var server = new ShrinkNetworkService();
|
||||
var a = new ShrinkLoopbackTransport(); var b = new ShrinkLoopbackTransport(); a.LinkPeer(b);
|
||||
foreach (var service in new[] {client, server})
|
||||
{
|
||||
service.RegisterMessage<PlayerContracts.Request>(1); service.RegisterMessage<PlayerContracts.Response>(2);
|
||||
service.RegisterMessage<PlayerContracts.Message>(3);
|
||||
}
|
||||
server.RegisterRequestHandler<PlayerContracts.Request, PlayerContracts.Response>((_, request) => UniTask.FromResult(new PlayerContracts.Response {Text=request.Text}));
|
||||
var delivered=0;
|
||||
server.RegisterHandler<PlayerContracts.Message>((_, message) => { delivered++; return UniTask.CompletedTask; });
|
||||
client.BindTransport(a); server.BindTransport(b);
|
||||
for (var i=0;i<sessions;i++) a.OpenSession(i);
|
||||
try
|
||||
{
|
||||
var request = new PlayerContracts.Request {Text=new string('x',bytes)}; var cursor=0;
|
||||
Measure($"loopback-rpc/bytes={bytes}/sessions={sessions}", 128, () => client.CallAsync<PlayerContracts.Request,PlayerContracts.Response>(client.Sessions[cursor++%sessions],request).GetAwaiter().GetResult());
|
||||
var payload = new ShrinkJsonNetworkSerializer().Serialize(new PlayerContracts.Message {Text=request.Text});
|
||||
Measure($"loopback-broadcast/bytes={bytes}/sessions={sessions}", 20, () => {
|
||||
foreach(var peer in client.Sessions.Values) client.SendSerializedAsync(peer,typeof(PlayerContracts.Message),payload).GetAwaiter().GetResult();
|
||||
});
|
||||
Assert.That(delivered,Is.EqualTo(50*sessions));
|
||||
}
|
||||
finally {a.Stop();b.Stop();}
|
||||
await UniTask.Yield();
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void ContextAndEventMatrix()
|
||||
{
|
||||
foreach(var count in new[] {10,100,1000})
|
||||
{
|
||||
var runtime=new ShrinkContextRuntime();
|
||||
try
|
||||
{
|
||||
for(var i=0;i<count;i++) runtime.Use(new Component("k"+i,i==0?null:"k"+(i-1)));
|
||||
Measure("context-diagnostic/components="+count,100,()=>runtime.CaptureDiagnostic());
|
||||
}
|
||||
finally {runtime.ShutdownAsync().GetAwaiter().GetResult();}
|
||||
}
|
||||
using(var binding=ShrinkGeneratedBinding.SubscribeAsync<PlayerContracts.Event>(ShrinkGeneratedBinding.RuntimeResolver,null,null,null,(_,__)=>UniTask.CompletedTask,ShrinkEventPriority.Normal,0,false))
|
||||
{
|
||||
Measure("event-async-completed",10000,()=>EventBus.PostAsync(new PlayerContracts.Event()).GetAwaiter().GetResult());
|
||||
Measure("event-bindings",1000,()=>EventBus.CaptureBindings(EventBus.Default));
|
||||
}
|
||||
Measure("event-register-withdraw",10000,()=>ShrinkGeneratedBinding.Subscribe<PlayerContracts.Event>(ShrinkGeneratedBinding.RuntimeResolver,null,null,null,_=>{},ShrinkEventPriority.Normal,0,false).Dispose());
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator SlowConsumerAndBurst() => UniTask.ToCoroutine(async () =>
|
||||
{
|
||||
using var queue=new ShrinkNetworkWorkQueue(64,65536);
|
||||
var gate=new UniTaskCompletionSource();
|
||||
var slow=queue.EnqueueAsync(1,"reliable",1024,()=>gate.Task);
|
||||
var pumping=queue.PumpAsync(64,65536,TimeSpan.FromMilliseconds(1));
|
||||
var results=new List<UniTask<ShrinkNetworkQueueResult>>();
|
||||
for(var i=0;i<128;i++) results.Add(queue.EnqueueAsync(i%32,"state",1024,()=>UniTask.CompletedTask,"position"));
|
||||
var before=queue.CaptureDiagnostics();
|
||||
Assert.That(before.Replaced,Is.EqualTo(96));
|
||||
await UniTask.Delay(10);
|
||||
Assert.That(pumping.Status,Is.EqualTo(UniTaskStatus.Pending));
|
||||
gate.TrySetResult(); await pumping; await slow;
|
||||
await queue.PumpAsync(64);
|
||||
foreach(var result in results) await result;
|
||||
Assert.That(queue.PendingCount,Is.Zero);
|
||||
UnityEngine.Debug.Log("PLAYER Queue pending="+before.PendingCount+" bytes="+before.PendingBytes+" replaced="+before.Replaced+" rejected="+before.Rejected);
|
||||
});
|
||||
|
||||
private sealed class Component : IShrinkComponent
|
||||
{
|
||||
private readonly string _key;
|
||||
public Component(string key,string dependency) { _key=key; Inject=dependency==null?Array.Empty<string>():new[]{dependency}; Provide=new[]{key}; }
|
||||
public string Name=>_key;
|
||||
public IReadOnlyList<string> Inject {get;}
|
||||
public IReadOnlyList<string> Provide {get;}
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx,object config) {ctx.Set(_key,this);return UniTask.CompletedTask;}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a11773fcfc6a5547942e45a40ce1c30
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04969664a502f284e94ccfd427fc052c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f13c1997933654439e4d4ffa612ae4c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ShrinkSDK.AgentSupport.PlayerTests",
|
||||
"references": [
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"ShrinkEventBus.Runtime",
|
||||
"ShrinkNetwork.Runtime",
|
||||
"UniTask",
|
||||
"ShrinkDataSaver.Runtime"
|
||||
],
|
||||
"optionalUnityReferences": [
|
||||
"TestAssemblies"
|
||||
],
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"autoReferenced": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec966393c67534f4d9331d20dae2a555
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,9 +2,12 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
@@ -13,30 +16,11 @@ namespace ShrinkSDK.WorkspaceValidation
|
||||
{
|
||||
public static class ShrinkSdkWorkspaceValidation
|
||||
{
|
||||
private static readonly ExpectedPackage[] ExpectedPackages =
|
||||
{
|
||||
new ExpectedPackage("ShrinkApp.Core", "com.cneicy.shrink-app-core", "0.1.1"),
|
||||
new ExpectedPackage("ShrinkApp.Starter.Basic", "com.cneicy.shrink-app-starter-basic", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkCommand", "com.cneicy.shrink-command", "0.2.0"),
|
||||
new ExpectedPackage("ShrinkCommand.Integration.App", "com.cneicy.shrink-command-integration-app", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkCommand.Integration.EventBus", "com.cneicy.shrink-command-integration-eventbus", "0.1.1"),
|
||||
new ExpectedPackage("ShrinkCommand.Integration.Network", "com.cneicy.shrink-command-integration-network", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkContext.AppAdapter", "com.cneicy.shrink-context-app-adapter", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkContext.Core", "com.cneicy.shrink-context-core", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkContext.EventBusAdapter", "com.cneicy.shrink-context-eventbus-adapter", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkDataSaver", "com.cneicy.shrink-datasaver", "2.2.0"),
|
||||
new ExpectedPackage("ShrinkDataSaver.Integration.App", "com.cneicy.shrink-datasaver-integration-app", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkDataSaver.Integration.EventBus", "com.cneicy.shrink-datasaver-integration-eventbus", "2.1.0"),
|
||||
new ExpectedPackage("ShrinkEventBus", "com.cneicy.shrink-eventbus", "2.0.0"),
|
||||
new ExpectedPackage("ShrinkEventBus.Entities", "com.cneicy.shrink-eventbus-entities", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkInstaller", "com.cneicy.shrink-installer", "0.1.2"),
|
||||
new ExpectedPackage("ShrinkModFramework", "com.cneicy.shrink-mod-framework", "0.2.1"),
|
||||
new ExpectedPackage("ShrinkNetwork", "com.cneicy.shrink-network", "0.2.0"),
|
||||
new ExpectedPackage("ShrinkNetwork.Integration.App", "com.cneicy.shrink-network-integration-app", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkNetwork.Integration.EventBus", "com.cneicy.shrink-network-integration-eventbus", "0.1.1"),
|
||||
new ExpectedPackage("ShrinkShared.CodeGen", "com.cneicy.shrink-shared-codegen", "0.1.0"),
|
||||
new ExpectedPackage("ShrinkTutorial", "com.cneicy.shrink-tutorial", "0.1.0")
|
||||
};
|
||||
private const string InstallerPackageName = "com.cneicy.shrink-installer";
|
||||
private const string InstallerCatalogTypeName = "ShrinkSDK.Installer.ShrinkSdkPackageCatalog";
|
||||
private static readonly Regex SemanticVersion = new Regex(
|
||||
@"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
@@ -45,6 +29,7 @@ namespace ShrinkSDK.WorkspaceValidation
|
||||
var workspaceRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
var packages = ReadPackages(workspaceRoot);
|
||||
ValidateGraph(packages);
|
||||
ValidateInstallerCatalog(packages);
|
||||
Debug.Log($"ShrinkSDK Workspace validation passed: packages={packages.Count}");
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
@@ -57,36 +42,97 @@ namespace ShrinkSDK.WorkspaceValidation
|
||||
|
||||
private static Dictionary<string, PackageDefinition> ReadPackages(string workspaceRoot)
|
||||
{
|
||||
var result = new Dictionary<string, PackageDefinition>(StringComparer.Ordinal);
|
||||
foreach (var expected in ExpectedPackages)
|
||||
var modulesRoot = Path.Combine(workspaceRoot, "Assets", "Modules");
|
||||
if (!Directory.Exists(modulesRoot))
|
||||
{
|
||||
var manifestPath = Path.Combine(workspaceRoot, "Assets", "Modules", expected.Directory, "package.json");
|
||||
throw new DirectoryNotFoundException($"Workspace package directory is missing: {modulesRoot}");
|
||||
}
|
||||
|
||||
var declaredDirectories = ReadDeclaredPackageDirectories(workspaceRoot);
|
||||
var manifestDirectories = Directory.EnumerateDirectories(modulesRoot)
|
||||
.Where(directory => File.Exists(Path.Combine(directory, "package.json")))
|
||||
.Select(Path.GetFullPath)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var undeclared = manifestDirectories
|
||||
.Where(directory => !declaredDirectories.Contains(directory))
|
||||
.OrderBy(directory => directory, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if (undeclared.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Package directories are not declared as submodules: " + string.Join(", ", undeclared));
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, PackageDefinition>(StringComparer.Ordinal);
|
||||
foreach (var directory in declaredDirectories.OrderBy(value => value, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var manifestPath = Path.Combine(directory, "package.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Required package manifest is missing: {expected.Directory}", manifestPath);
|
||||
throw new FileNotFoundException("Declared package submodule has no package.json.", manifestPath);
|
||||
}
|
||||
|
||||
var manifest = JObject.Parse(File.ReadAllText(manifestPath));
|
||||
var packageName = manifest.Value<string>("name");
|
||||
var version = manifest.Value<string>("version");
|
||||
if (packageName == null || version == null || packageName.Length == 0 || version.Length == 0)
|
||||
if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
throw new InvalidOperationException($"{expected.Directory} has no valid package name or version.");
|
||||
throw new InvalidOperationException($"{directory} has no valid package name or version.");
|
||||
}
|
||||
|
||||
if (!string.Equals(packageName, expected.Name, StringComparison.Ordinal) ||
|
||||
!string.Equals(version, expected.Version, StringComparison.Ordinal))
|
||||
var validPackageName = packageName!;
|
||||
var validVersion = version!;
|
||||
if (!SemanticVersion.IsMatch(validVersion))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{expected.Directory} must be {expected.Name}@{expected.Version}, found {packageName}@{version}.");
|
||||
throw new InvalidOperationException($"{validPackageName} has an invalid semantic version: {validVersion}");
|
||||
}
|
||||
|
||||
if (result.ContainsKey(packageName))
|
||||
if (result.ContainsKey(validPackageName))
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate package manifest name: {packageName}");
|
||||
throw new InvalidOperationException($"Duplicate package manifest name: {validPackageName}");
|
||||
}
|
||||
|
||||
result.Add(packageName, new PackageDefinition(packageName, version, manifest));
|
||||
result.Add(validPackageName, new PackageDefinition(validPackageName, validVersion, manifest));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static HashSet<string> ReadDeclaredPackageDirectories(string workspaceRoot)
|
||||
{
|
||||
var gitModulesPath = Path.Combine(workspaceRoot, ".gitmodules");
|
||||
if (!File.Exists(gitModulesPath))
|
||||
{
|
||||
throw new FileNotFoundException("Workspace .gitmodules is missing.", gitModulesPath);
|
||||
}
|
||||
|
||||
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var line in File.ReadLines(gitModulesPath))
|
||||
{
|
||||
var match = Regex.Match(line, @"^\s*path\s*=\s*(?<path>.+?)\s*$", RegexOptions.CultureInvariant);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relativePath = match.Groups["path"].Value.Replace('\\', '/');
|
||||
if (!relativePath.StartsWith("Assets/Modules/", StringComparison.Ordinal) ||
|
||||
relativePath.Substring("Assets/Modules/".Length).Contains("/"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(Path.Combine(workspaceRoot, relativePath));
|
||||
if (!result.Add(fullPath))
|
||||
{
|
||||
throw new InvalidOperationException($"Duplicate package submodule path: {relativePath}");
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No Assets/Modules package submodules are declared.");
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -158,6 +204,63 @@ namespace ShrinkSDK.WorkspaceValidation
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateInstallerCatalog(IReadOnlyDictionary<string, PackageDefinition> packages)
|
||||
{
|
||||
var catalogType = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(assembly => assembly.GetType(InstallerCatalogTypeName, false))
|
||||
.FirstOrDefault(type => type != null)
|
||||
?? throw new InvalidOperationException($"Installer catalog type was not loaded: {InstallerCatalogTypeName}");
|
||||
var packagesField = catalogType.GetField("Packages", BindingFlags.Static | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("Installer catalog Packages field was not found.");
|
||||
var catalogItems = packagesField.GetValue(null) as IEnumerable
|
||||
?? throw new InvalidOperationException("Installer catalog Packages field is not enumerable.");
|
||||
|
||||
var catalog = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var item in catalogItems)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new InvalidOperationException("Installer catalog contains a null package entry.");
|
||||
}
|
||||
|
||||
var itemType = item.GetType();
|
||||
var packageName = itemType.GetProperty("PackageName")?.GetValue(item) as string;
|
||||
var version = itemType.GetProperty("Version")?.GetValue(item) as string;
|
||||
if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
throw new InvalidOperationException("Installer catalog contains an invalid package entry.");
|
||||
}
|
||||
|
||||
var validPackageName = packageName!;
|
||||
var validVersion = version!;
|
||||
if (!catalog.TryAdd(validPackageName, validVersion))
|
||||
{
|
||||
throw new InvalidOperationException($"Installer catalog contains duplicate package: {validPackageName}");
|
||||
}
|
||||
}
|
||||
|
||||
var expectedNames = packages.Keys
|
||||
.Where(name => !string.Equals(name, InstallerPackageName, StringComparison.Ordinal))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var missing = expectedNames.Except(catalog.Keys).OrderBy(name => name, StringComparer.Ordinal).ToArray();
|
||||
var unexpected = catalog.Keys.Except(expectedNames).OrderBy(name => name, StringComparer.Ordinal).ToArray();
|
||||
if (missing.Length > 0 || unexpected.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Installer catalog package set differs from Workspace. Missing=[{string.Join(", ", missing)}], " +
|
||||
$"unexpected=[{string.Join(", ", unexpected)}]");
|
||||
}
|
||||
|
||||
foreach (var packageName in expectedNames)
|
||||
{
|
||||
if (!string.Equals(catalog[packageName], packages[packageName].Version, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Installer catalog requires {packageName} {catalog[packageName]}, but the Workspace provides {packages[packageName].Version}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PackageDefinition
|
||||
{
|
||||
public PackageDefinition(string name, string version, JObject manifest)
|
||||
@@ -171,20 +274,6 @@ namespace ShrinkSDK.WorkspaceValidation
|
||||
public string Version { get; }
|
||||
public JObject Manifest { get; }
|
||||
}
|
||||
|
||||
private readonly struct ExpectedPackage
|
||||
{
|
||||
public ExpectedPackage(string directory, string name, string version)
|
||||
{
|
||||
Directory = directory;
|
||||
Name = name;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public string Directory { get; }
|
||||
public string Name { get; }
|
||||
public string Version { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Submodule Assets/Modules/ShrinkApp.Core updated: 35acaca858...5efc172bca
Submodule Assets/Modules/ShrinkApp.Starter.Basic updated: 88e70eef0e...fd3788e2ce
Submodule Assets/Modules/ShrinkCommand updated: c35ddd668f...174b9c6976
Submodule Assets/Modules/ShrinkCommand.Integration.App updated: aa08408527...25f4cfb82e
Submodule Assets/Modules/ShrinkCommand.Integration.EventBus updated: acd733a8ae...82c0833a11
Submodule Assets/Modules/ShrinkCommand.Integration.Network updated: 78576b507d...b1c42ea4ab
Submodule Assets/Modules/ShrinkContext.AppAdapter updated: 36790a03ae...7a5071217d
Submodule Assets/Modules/ShrinkContext.Core updated: f50020134c...d3eca1f155
Submodule Assets/Modules/ShrinkContext.EventBusAdapter updated: 229c6cfe70...92f55f4bdf
Submodule Assets/Modules/ShrinkDataSaver updated: 81ab5c09cb...cdb682f484
Submodule Assets/Modules/ShrinkDataSaver.Integration.App updated: 519698b062...fea752a8ff
Submodule Assets/Modules/ShrinkDataSaver.Integration.EventBus updated: 014bf8d56d...d6b1940982
Submodule Assets/Modules/ShrinkEventBus updated: 5c5de37fdf...a92c9d2cea
Submodule Assets/Modules/ShrinkEventBus.Entities updated: 984aaebe9c...c19d536582
Submodule Assets/Modules/ShrinkInstaller updated: 7de7bbbcdc...2e03045423
Submodule Assets/Modules/ShrinkModFramework updated: 5e9f501c26...91f0141b86
Submodule Assets/Modules/ShrinkNetwork updated: e8407f1719...93c2b8520b
Submodule Assets/Modules/ShrinkNetwork.Integration.App updated: 619604e049...123bd170d9
Submodule Assets/Modules/ShrinkNetwork.Integration.EventBus updated: a6fb3db20b...2dae3e3890
+1
Submodule Assets/Modules/ShrinkRuntime.Abstractions added at 52b92d3657
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10936b7d408f403449df0946277a2d00
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Submodule Assets/Modules/ShrinkShared.CodeGen updated: a2d2b8da7d...c669ee5956
Submodule Assets/Modules/ShrinkTutorial updated: 98c43fc193...51c941a017
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13ee6beaa5cda4a4da34954ba6f00e7a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><TargetFramework>netstandard2.1</TargetFramework><LangVersion>latest</LangVersion><Nullable>enable</Nullable><EnableDefaultCompileItems>false</EnableDefaultCompileItems><PackageId>ShrinkSDK.Inspection</PackageId><Version>0.1.0</Version><AssemblyName>ShrinkInspection.Runtime</AssemblyName></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="../Runtime/**/*.cs" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="ShrinkSDK.Context.Core" Version="0.3.0" />
|
||||
<PackageReference Include="ShrinkSDK.EventBus" Version="2.2.2" />
|
||||
<PackageReference Include="ShrinkSDK.Network" Version="0.4.2" />
|
||||
<PackageReference Include="ShrinkSDK.Command" Version="0.4.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2274a33f3eae9774db82c4e478f2f90d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "ShrinkInspection.Editor",
|
||||
"references": ["ShrinkInspection.Runtime", "ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "Newtonsoft.Json"],
|
||||
"includePlatforms": ["Editor"],
|
||||
"autoReferenced": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a20e065e3e35a94e9e3c1c29328219f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation;
|
||||
using UnityEngine;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkSDK.Inspection.Editor
|
||||
{
|
||||
public static class ShrinkInspectionExport
|
||||
{
|
||||
private const string DirectoryName = "Artifacts/Inspection";
|
||||
/// <summary>Host extensions add explicitly owned Network/Command/Context instances. Remove handlers when the host shuts down.</summary>
|
||||
public static event Action<ShrinkInspectionSnapshot>? CaptureHostState;
|
||||
|
||||
[MenuItem("Tools/ShrinkSDK/Inspection/Export compilation inputs")]
|
||||
public static void ExportCompilation()
|
||||
{
|
||||
RequireStableEditor();
|
||||
var root = Path.GetFullPath(".");
|
||||
var assemblies = CompilationPipeline.GetAssemblies(AssembliesType.Player);
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.GetName().Name).ToHashSet();
|
||||
var identityNames = assemblies.Select(a => a.name).Where(loaded.Contains).OrderBy(n => n, StringComparer.Ordinal).ToArray();
|
||||
var inputs = assemblies.Select(a => new { Name = a.name, Sources = a.sourceFiles.Select(Path.GetFullPath).ToArray(),
|
||||
References = a.allReferences.Select(Path.GetFullPath).Where(File.Exists).ToArray(), Defines = a.defines, Output = Path.GetFullPath(a.outputPath) }).ToArray();
|
||||
var ticks = inputs.SelectMany(a => a.Sources.Concat(a.References)).Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(path => path, path => File.GetLastWriteTimeUtc(path).Ticks);
|
||||
Directory.CreateDirectory(DirectoryName);
|
||||
File.WriteAllText(DirectoryName + "/compilation.json", JsonConvert.SerializeObject(new { Root = root,
|
||||
BuildId = ShrinkInspectionSnapshot.BuildIdentity(identityNames), IdentityAssemblies = identityNames, Assemblies = inputs, SourceTicks = ticks }, Formatting.Indented));
|
||||
Debug.Log("Shrink inspection compilation exported: " + Path.GetFullPath(DirectoryName + "/compilation.json"));
|
||||
}
|
||||
|
||||
[MenuItem("Tools/ShrinkSDK/Inspection/Export runtime snapshot")]
|
||||
public static void ExportRuntime()
|
||||
{
|
||||
RequireStableEditor();
|
||||
var path = DirectoryName + "/compilation.json";
|
||||
if (!File.Exists(path)) throw new InvalidOperationException("Export compilation inputs first.");
|
||||
var manifest = JsonConvert.DeserializeObject<IdentityManifest>(File.ReadAllText(path))!;
|
||||
var snapshot = new ShrinkInspectionSnapshot(ShrinkInspectionSnapshot.BuildIdentity(manifest.IdentityAssemblies));
|
||||
snapshot.AddContext("default-context", ShrinkContextDefaults.Default);
|
||||
var ordinal = 0;
|
||||
foreach (var bus in EventBus.CaptureNamedBuses()) snapshot.AddEventBus("named-bus-" + ordinal++, bus);
|
||||
snapshot.Coverage.Add("Default Context and named EventBus instances only; host-owned instances require CaptureHostState registration.");
|
||||
CaptureHostState?.Invoke(snapshot);
|
||||
File.WriteAllText(DirectoryName + "/runtime.json", snapshot.ToJson());
|
||||
Debug.Log("Shrink inspection runtime exported: " + Path.GetFullPath(DirectoryName + "/runtime.json"));
|
||||
}
|
||||
private static void RequireStableEditor()
|
||||
{
|
||||
if (EditorApplication.isCompiling || EditorApplication.isUpdating) throw new InvalidOperationException("Wait for Editor compilation/import to finish.");
|
||||
}
|
||||
private sealed class IdentityManifest { public string[] IdentityAssemblies = Array.Empty<string>(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c6f0febfce34543a9efb401ce8b3b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
# Shrink Inspection
|
||||
|
||||
可选诊断接入包,提供 Unity 编译输入导出和跨 Unity/Godot/.NET 的按需绑定快照。它依赖被观察模块,不进入它们的热路径。
|
||||
|
||||
Unity 安装本目录的 UPM 包后,使用 `Tools/ShrinkSDK/Inspection` 导出编译输入和运行快照。结果位于项目的 `Artifacts/Inspection`。在命令行使用:
|
||||
|
||||
```text
|
||||
shrink inspect flow command:execute --compilation Artifacts/Inspection/compilation.json --snapshot Artifacts/Inspection/runtime.json
|
||||
```
|
||||
|
||||
编辑器默认只捕获默认 Context 与命名 EventBus。宿主持有的独立实例通过 `ShrinkInspectionExport.CaptureHostState` 按需加入;宿主退出时取消该委托,避免诊断代码持有已退役对象。`AddContext` 可同时传入 Loader,记录最近事务的阶段和恢复结果。
|
||||
|
||||
Godot/.NET 使用 `ShrinkSDK.Inspection` 和相同的 `ShrinkInspectionSnapshot` API。宿主在所属线程创建快照,调用 `AddContext`、`AddNetwork`、`AddEventBus`、`AddCommands`,再写入 `ToJson()`。构建标识由明确的程序集名集合计算;必须与编译输入的程序集集合和 MVID 一致。未知构建不能通过手写相同字符串伪装为已验证关联。
|
||||
|
||||
捕获时暂停同一实例的注册、退役和组合修改。快照只含符号、状态和计数;不含消息正文、会话令牌、存档内容及异常文本。它记录快照时刻的绑定,不提供完整调用时序。不同 scope 的绑定不能拼成已经确认的端到端调用。
|
||||
|
||||
<!-- shrink:capabilities -->
|
||||
Capability: 导出真实编译输入、Context/provider 状态、EventBus/Network/Command 当前绑定及队列指标
|
||||
Aliases: 导航 调试 诊断 快照 inspect snapshot 调用链
|
||||
Limits: 按需快照,不是调用时序追踪;只覆盖显式加入的实例;过期或不同构建不能合并
|
||||
Extension: 宿主在所属线程调用 AddContext/AddNetwork/AddEventBus/AddCommands,Unity 使用 CaptureHostState
|
||||
Evidence: [快照 API](Runtime/ShrinkInspectionSnapshot.cs); [Unity 导出](Editor/ShrinkInspectionExport.cs)
|
||||
<!-- /shrink:capabilities -->
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f566b821eafe5fa498c7504ccf0d40aa
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87749fd61a2f5e44aab3896a7a873b38
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "ShrinkInspection.Runtime",
|
||||
"references": ["ShrinkContext.Core.Runtime", "ShrinkEventBus.Runtime", "ShrinkNetwork.Runtime", "ShrinkCommand.Runtime", "Newtonsoft.Json"],
|
||||
"autoReferenced": true,
|
||||
"noEngineReferences": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4210356b49bcdf240ad3c08659b7f720
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,102 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkCommand;
|
||||
|
||||
namespace ShrinkSDK.Inspection
|
||||
{
|
||||
public sealed class ShrinkInspectionRelation
|
||||
{
|
||||
public string From = "";
|
||||
public string To = "";
|
||||
public string Kind = "";
|
||||
public string Scope = "";
|
||||
}
|
||||
|
||||
/// <summary>Explicit, on-demand snapshot. Call from the owning host thread while registration/composition is stable.</summary>
|
||||
public sealed class ShrinkInspectionSnapshot
|
||||
{
|
||||
public int FormatVersion = 1;
|
||||
public string BuildId = "";
|
||||
public string CapturedAtUtc = DateTime.UtcNow.ToString("O");
|
||||
public readonly List<string> Coverage = new();
|
||||
public readonly List<ShrinkInspectionRelation> Relations = new();
|
||||
public readonly Dictionary<string, object> State = new();
|
||||
|
||||
public static string BuildIdentity(IEnumerable<string> assemblyNames)
|
||||
{
|
||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray();
|
||||
var identity = string.Join(";", assemblyNames.Distinct().OrderBy(n => n, StringComparer.Ordinal).Select(name =>
|
||||
{
|
||||
var candidates = loaded.Where(a => a.GetName().Name == name).ToArray();
|
||||
if (candidates.Length != 1) throw new InvalidOperationException("Cannot establish unique runtime assembly identity: " + name);
|
||||
return name + ":" + candidates[0].ManifestModule.ModuleVersionId.ToString("N");
|
||||
}));
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
return BitConverter.ToString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(identity))).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
public ShrinkInspectionSnapshot(string buildId) { BuildId = buildId; }
|
||||
public string ToJson() => JsonConvert.SerializeObject(this, Formatting.Indented, new Newtonsoft.Json.Converters.StringEnumConverter());
|
||||
|
||||
public void AddContext(string scope, ShrinkContextRuntime runtime, ShrinkContextLoader? loader = null)
|
||||
{
|
||||
var snapshot = runtime.CaptureDiagnostic();
|
||||
State.Add(scope, new { snapshot.BindingCount, snapshot.Notifications,
|
||||
Fibers = snapshot.Fibers.Select(f => new { f.Uid, f.Name, f.Status, f.Retired, f.InTransition, f.Inject, f.Provide, f.Dependencies, f.ErrorType }).ToArray(),
|
||||
Transaction = loader?.LastTransaction is { } t ? new { t.Generation, t.Phase, t.CurrentEntryId, t.ErrorType, t.RestoreAttempted, t.PreviousCompositionRestored, t.RestoreErrorType } : null });
|
||||
foreach (var fiber in snapshot.Fibers.Where(f => !f.Retired))
|
||||
foreach (var dependency in fiber.Dependencies)
|
||||
{
|
||||
Add("fiber:" + scope + ":" + fiber.Uid, "key:" + dependency.Key, "requires@" + dependency.Realm, scope);
|
||||
if (dependency.CurrentProviderUid.HasValue)
|
||||
Add("fiber:" + scope + ":" + dependency.CurrentProviderUid, "fiber:" + scope + ":" + fiber.Uid, "provider:" + dependency.Key + "@" + dependency.Realm, scope);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddNetwork(string scope, ShrinkNetworkService service)
|
||||
{
|
||||
State.Add(scope, new { Metrics = service.GetDiagnosticsSnapshot(), Queue = service.DispatchScheduler is ShrinkNetworkDispatchQueue queued ? queued.CaptureDiagnostics() : (service.DispatchScheduler as ShrinkNetworkInlineDispatchScheduler)?.CaptureDiagnostics() });
|
||||
foreach (var meta in service.MessageRegistry.Registrations)
|
||||
{
|
||||
Add("opcode:" + meta.Opcode, "type:" + TypeName(meta.MessageType), "message", scope);
|
||||
if (!string.IsNullOrEmpty(meta.Route)) Add("route:" + meta.Route, "type:" + TypeName(meta.MessageType), "message", scope);
|
||||
}
|
||||
foreach (var pair in service.Router.CaptureBindings()) Add("type:" + TypeName(pair.Key), MethodId(pair.Value), "handler", scope);
|
||||
}
|
||||
|
||||
public void AddEventBus(string scope, IShrinkEventBus bus)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bindings = EventBus.CaptureBindings(bus);
|
||||
State.Add(scope, new { Bus = bus.Key.ToString(), Subscribers = bindings.Count });
|
||||
foreach (var binding in bindings) Add("type:" + TypeName(binding.EventType), MethodId(binding.Handler), "subscribes", scope);
|
||||
}
|
||||
catch (NotSupportedException) { Coverage.Add(scope + ": custom EventBus does not expose bindings."); }
|
||||
}
|
||||
|
||||
public void AddCommands(string scope, ShrinkCommandService service)
|
||||
{
|
||||
State.Add(scope, new { Registrations = service.Commands.Select(c => new { c.Path, c.Aliases, c.Permission, c.SourceKind }).ToArray() });
|
||||
foreach (var binding in service.CaptureBindings()) Add("command:" + binding.Path, MethodId(binding.Handler), "command", scope);
|
||||
}
|
||||
|
||||
private void Add(string from, string to, string kind, string scope) => Relations.Add(new ShrinkInspectionRelation { From = from, To = to, Kind = kind, Scope = scope });
|
||||
private static string TypeName(Type type) => (type.FullName ?? type.Name).Replace('+', '.');
|
||||
public static string MethodId(MethodInfo method)
|
||||
{
|
||||
// Generated wrappers and generic signatures remain runtime-only endpoints rather than guessed source matches.
|
||||
if (method.IsGenericMethod || method.DeclaringType?.IsGenericType == true || method.GetParameters().Any(p => p.ParameterType.IsGenericType || p.ParameterType.IsArray || p.ParameterType.IsByRef))
|
||||
return "runtime-method:" + method.Module.ModuleVersionId.ToString("N") + ":" + method.MetadataToken;
|
||||
var parameters = method.GetParameters();
|
||||
return "M:" + TypeName(method.DeclaringType!) + "." + method.Name + (parameters.Length == 0 ? "" : "(" + string.Join(",", parameters.Select(p => TypeName(p.ParameterType))) + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a412b2f6a531a33419d270e47853ef05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-inspection",
|
||||
"version": "0.1.0",
|
||||
"displayName": "Shrink Inspection",
|
||||
"unity": "2022.3",
|
||||
"description": "On-demand build inputs and bounded runtime binding snapshots for shrink inspect.",
|
||||
"dependencies": {
|
||||
"com.cneicy.shrink-context-core": "0.3.0",
|
||||
"com.cneicy.shrink-eventbus": "2.2.2",
|
||||
"com.cneicy.shrink-network": "0.4.2",
|
||||
"com.cneicy.shrink-command": "0.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdb80de9a8588bc4392a94dd7352be58
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -65,7 +65,7 @@ ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK Workspace,不
|
||||
|
||||
```text
|
||||
ShrinkSDK Workspace/
|
||||
|-- Assets/Modules/ 21 个同路径 Git submodule UPM 包
|
||||
|-- Assets/Modules/ .gitmodules 声明的同路径 Git submodule UPM 包
|
||||
|-- Assets/Modules/*.meta 根仓库追踪的 Unity 目录 GUID
|
||||
|-- Assets/Scenes/ 示例与验收场景
|
||||
|-- Assets/Resources/ 当前应用配置与组合 Profile
|
||||
@@ -73,6 +73,7 @@ ShrinkSDK Workspace/
|
||||
|-- GeneratedModSdk/ Mod SDK 导出物
|
||||
|-- Packages/ 根 Unity 工程依赖
|
||||
|-- Tools/UpmConsumerValidation/ 干净 UPM 消费工程验证
|
||||
|-- Tools/Release/ 包版本、依赖与目录同步工具
|
||||
|-- Tools/RepositoryMigration/ 子模块、发布仓库与独立宿主初始化脚本
|
||||
|-- Docs/Archive/ 已完成迁移与过期地图,仅供追溯
|
||||
|-- DESIGN.md 唯一当前架构文档
|
||||
@@ -84,27 +85,28 @@ ShrinkSDK Workspace/
|
||||
|
||||
| 包 | 版本 | 职责 |
|
||||
|---|---:|---|
|
||||
| `com.cneicy.shrink-eventbus` | 2.0.0 | 单一事件模型、多 Bus、生成特性订阅、UniTask 调度与零 GC 热路径 |
|
||||
| `com.cneicy.shrink-eventbus-entities` | 0.1.0 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 |
|
||||
| `com.cneicy.shrink-datasaver` | 2.2.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 |
|
||||
| `com.cneicy.shrink-command` | 0.2.0 | 路径式命令、权限与同步/异步执行 |
|
||||
| `com.cneicy.shrink-network` | 0.2.0 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 |
|
||||
| `com.cneicy.shrink-mod-framework` | 0.2.1 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease |
|
||||
| `com.cneicy.shrink-tutorial` | 0.1.0 | 数据驱动引导、遮罩、锚点、触发与持久化 |
|
||||
| `com.cneicy.shrink-context-core` | 0.1.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 |
|
||||
| `com.cneicy.shrink-app-core` | 0.1.1 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 |
|
||||
| `com.cneicy.shrink-app-starter-basic` | 0.1.0 | 默认 Context 组合根、配置资产和示例入口 |
|
||||
| `com.cneicy.shrink-context-app-adapter` | 0.1.0 | ContextLoader 宿主、Profile/JSON、诊断与基准 |
|
||||
| `com.cneicy.shrink-context-eventbus-adapter` | 0.1.0 | EventBus 生成绑定的可逆 EffectAttach 包装 |
|
||||
| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.1.0 | DataSaver 事件桥 |
|
||||
| `com.cneicy.shrink-datasaver-integration-app` | 0.1.0 | DataSaver App installer/原生 Context 组件 |
|
||||
| `com.cneicy.shrink-command-integration-eventbus` | 0.1.1 | 命令请求与生命周期事件桥 |
|
||||
| `com.cneicy.shrink-command-integration-network` | 0.1.0 | `command/execute` RPC 桥 |
|
||||
| `com.cneicy.shrink-command-integration-app` | 0.1.0 | Command App installer/原生 Context 组件 |
|
||||
| `com.cneicy.shrink-network-integration-eventbus` | 0.1.1 | 网络事件广播、裁决结果与 delta 去重 |
|
||||
| `com.cneicy.shrink-network-integration-app` | 0.1.0 | Network App installer/原生 Context 组件 |
|
||||
| `com.cneicy.shrink-shared-codegen` | 0.1.0 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 |
|
||||
| `com.cneicy.shrink-installer` | 0.1.2 | 安全合并公开 registry、显示诊断并固定版本安装 Starter 或选定模块的 Editor 引导包 |
|
||||
| `com.cneicy.shrink-eventbus` | 2.2.2 | 单一事件模型、多 Bus、生成特性订阅、可选 MonoBehaviour 生命周期织入、UniTask 调度与零 GC 热路径 |
|
||||
| `com.cneicy.shrink-eventbus-entities` | 0.1.5 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 |
|
||||
| `com.cneicy.shrink-datasaver` | 2.4.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 |
|
||||
| `com.cneicy.shrink-command` | 0.4.2 | 路径式命令、权限与同步/异步执行 |
|
||||
| `com.cneicy.shrink-network` | 0.4.2 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 |
|
||||
| `com.cneicy.shrink-mod-framework` | 0.3.6 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease |
|
||||
| `com.cneicy.shrink-tutorial` | 0.2.0 | 数据驱动引导、遮罩、锚点、触发与持久化 |
|
||||
| `com.cneicy.shrink-context-core` | 0.3.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 |
|
||||
| `com.cneicy.shrink-app-core` | 0.2.3 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 |
|
||||
| `com.cneicy.shrink-app-starter-basic` | 0.3.7 | 默认 Context 组合根、配置资产和示例入口 |
|
||||
| `com.cneicy.shrink-context-app-adapter` | 0.2.4 | ContextLoader 宿主、Profile/JSON、诊断与基准 |
|
||||
| `com.cneicy.shrink-context-eventbus-adapter` | 0.2.4 | EventBus 生成绑定的可逆 EffectAttach 包装 |
|
||||
| `com.cneicy.shrink-datasaver-integration-eventbus` | 2.2.5 | DataSaver 事件桥 |
|
||||
| `com.cneicy.shrink-datasaver-integration-app` | 0.2.5 | DataSaver App installer/原生 Context 组件 |
|
||||
| `com.cneicy.shrink-command-integration-eventbus` | 0.2.5 | 命令请求与生命周期事件桥 |
|
||||
| `com.cneicy.shrink-command-integration-network` | 0.2.5 | `command/execute` RPC 桥 |
|
||||
| `com.cneicy.shrink-command-integration-app` | 0.2.5 | Command App installer/原生 Context 组件 |
|
||||
| `com.cneicy.shrink-network-integration-eventbus` | 0.2.5 | 网络事件广播、裁决结果与 delta 去重 |
|
||||
| `com.cneicy.shrink-network-integration-app` | 0.2.5 | Network App installer/原生 Context 组件 |
|
||||
| `com.cneicy.shrink-shared-codegen` | 0.2.1 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 |
|
||||
| `com.cneicy.shrink-runtime-abstractions` | 0.1.0 | Unity 与其他 .NET 宿主共用的平台服务合同 |
|
||||
| `com.cneicy.shrink-installer` | 0.2.13 | UI Toolkit 包管理器:显示真实安装状态,按 Context 分层提供固定版本单包与推荐组合 |
|
||||
|
||||
`ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。
|
||||
|
||||
@@ -157,7 +159,7 @@ Cordis 迁移的阶段 0 至阶段 5 已进入当前架构,不再是待办计
|
||||
|
||||
`ShrinkAppLoaderBootstrapper.DefaultComposition` 接入 Basic Starter 的七个声明式模块:Command、DataSaver、Network 三个原生组件,Command-Network 集成,以及 Command/DataSaver/Network 的 EventBus 集成。
|
||||
|
||||
`ShrinkAppCompositionProfile` 用 ScriptableObject/JSON 决定条目启用、显式排除、isolate 与 intercept;组件工厂仍由代码注册,配置文件不能按任意类型名反射实例化对象。`ShrinkSDK/Cordis/诊断与组合` 显示 fiber、等待依赖、provider target、事务和外部程序集 revision。
|
||||
`ShrinkAppCompositionProfile` 用 ScriptableObject/JSON 决定条目启用、显式排除、isolate 与 intercept;组件工厂仍由代码注册,配置文件不能按任意类型名反射实例化对象。`ShrinkSDK/上下文/诊断与组合` 显示 fiber、等待依赖、provider target、事务和外部程序集 revision。
|
||||
|
||||
`ShrinkApp.IsRunning` 与 `ShrinkApp.Services` 已接回 LoaderHost。原生组件停用时使用 `TryUnregister(instance)` 撤回对应门面。旧 `IShrinkAppModuleInstaller` 适配仍可用于 ClassicHost,但默认 ContextLoader 不再通过它启动核心模块。
|
||||
|
||||
@@ -200,14 +202,16 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
|
||||
|
||||
### 8.1 Unity 运行时注册
|
||||
|
||||
`ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs` 为引用方程序集生成实例直接调用绑定和静态模块初始化注册,不运行时枚举方法。`ShrinkShared.CodeGen/Editor/ShrinkRegistryILPostProcessor.cs` 继续负责 Command、Network 和 App 注册表。外部 Mod DLL 必须携带 EventBus 生成合同;没有合同的生产 DLL 不自动反射注册。
|
||||
Unity 由 ShrinkShared.CodeGen 的 ShrinkRegistryILPostProcessor 作为唯一 IL 后处理入口,统一编织 EventBus、Command、Network 与 App 生成合同;旧分散入口保留兼容类但不再重复处理。运行时仍是生成的强类型绑定,不枚举方法。外部 Mod DLL 必须携带生成合同。
|
||||
|
||||
Unity 加载兼容其 Roslyn 3.8 的预构建分析器 DLL;独立 CLI 使用自己的较新 Roslyn,不能把相同源代码直接编译为仅支持新 API 的 DLL 后交给旧 Unity。SHRINK001–004 检查订阅签名、协议 opcode/route、installer 和常量 Context key。编译器分析不代替异步生命周期行为验证。
|
||||
|
||||
### 8.2 独立服务器生成
|
||||
|
||||
菜单入口:
|
||||
|
||||
- `ShrinkSDK/Network/生成完整独立服务器工程`
|
||||
- `ShrinkSDK/Network/刷新独立服务器 Generated 合同`
|
||||
- `ShrinkSDK/网络/生成完整独立服务器工程`
|
||||
- `ShrinkSDK/网络/刷新独立服务器生成合同`
|
||||
|
||||
完整生成分两步:
|
||||
|
||||
@@ -225,7 +229,7 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
|
||||
1. 内部包图检查:全部 `com.cneicy.*` 依赖版本一致、无循环,普通主包不反向依赖 Integration 包。
|
||||
2. Unity 编译:目标 asmdef 和根项目无编译错误。
|
||||
3. EditMode 测试:功能测试、Context 生命周期、语义扫描和模板覆写保护。
|
||||
4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 在递归 submodule 初始化后的 Workspace 中创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 21 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。发布后还必须分别验证 registry、精确 Git URL 和 Installer 三种空白消费者工程路径。
|
||||
4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 在递归 submodule 初始化后的 Workspace 中创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。发布后还必须分别验证 registry、精确 Git URL 和 Installer 三种空白消费者工程路径。
|
||||
5. 独立宿主:生成工程与 RuntimeSmoke 按变更范围构建或运行。
|
||||
6. 涉及真实生命周期时,仍需在目标场景执行 Play Mode 验收;源码检查和 EditMode 不能替代该路径。
|
||||
|
||||
@@ -253,8 +257,23 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效
|
||||
|
||||
- 当前架构只更新本文。
|
||||
- 包级使用方式和 API 示例放在各包 README。
|
||||
- `Tools/Release/Update-ShrinkSdkVersions.ps1` 负责包版本递增,并同步受影响的精确依赖、对应 NuGet 项目、Installer 目录、本文版本表和 `Tools/Release/package-versions.json`;依赖清单变化会触发依赖方 patch 版本递增。
|
||||
- `Tools/UpmConsumerValidation/published-upm-versions.json` 只记录已经发布且要接受真实消费验证的版本,不随未打标签的 Workspace 版本提前更新。
|
||||
- 包源码、标签、独立开发宿主与包级 CI 位于 `https://git.crash.work/ShrinkSDK/<Package>`;发布版本只能由与 `package.json.version` 一致的 `vX.Y.Z` 标签产生。
|
||||
- `NETWORK_PITFALLS.md` 记录实现经验,不描述当前模块清单。
|
||||
- `Docs/Archive/CORDIS_MIGRATION.completed.md` 保存迁移论证、阶段记录和历史验收数据。
|
||||
- `Docs/Archive/codebase-map-2026-05-23/` 保存迁移前代码地图,其中关于 Git 状态、模块规模和测试覆盖的描述均已过期。
|
||||
- `.planning/threads/` 只用于恢复工作上下文;其中历史 Notes 不得覆盖本文和当前源码。
|
||||
|
||||
|
||||
## 12. 本地导航与性能合同
|
||||
|
||||
- Tools/AgentSupport/Shrink.Inspect 是无模型 API 依赖的本地 Roslyn CLI;capabilities 用包级职责/入口/复用边界/不负责内容/验证点,symbol、flow、impact 使用语义边而不是把 grep 当调用图。
|
||||
- Unity 输入由 ShrinkInspection 导出实际源、引用、宏与构建标识;Godot/.NET 通过 MSBuildWorkspace 加载目标工程。static、candidate、runtime-bound、unresolved 分开显示;结果有数量、深度与分页上限,第三方引用和生成成员的缺口显式声明。
|
||||
- 运行快照只按需导出明确宿主的 Context、Network、EventBus 与 Command 绑定。fiber 标识带 scope;缺失 provider 是未满足依赖,不是假造绑定。快照不保存消息正文/令牌,不常驻连接生产环境。
|
||||
- Context 仅保留受管理的活跃/过渡 fiber;完全退役后移除运行时与父 effect 的实例引用,有界纯数据历史默认 64。依赖索引用于缩小诊断候选;同步深链转换以队列展开。
|
||||
- Network 使用 v2 SHK2 封装、消息体单次编码和明确池内存生命周期。自动派发为每服务有界串行队列;主线程手动泵送提供数量/字节/时间预算。可靠操作满队列明确失败,只有显式状态 key 可合并,RPC 响应独立完成以避免嵌套等待死锁。
|
||||
- MessagePack 为可选生成 resolver 适配器,不由核心反射发现 formatter。TCP 优化单次组合写入;没有把实验性 Pipelines 或 .NET 10 后端带入共享默认依赖。
|
||||
- DataSaver 主线程采集与后台快照编码分离;后台开关默认关闭,格式、原子替换与备份回退不变。阶段计时按需开启。
|
||||
|
||||
使用方式见 Tools/AgentSupport/README.md;新内容先执行 Docs/ReuseFirstDesign.md;跨组件可编译示例入口见 Docs/IntegrationExamples.md;性能结论与限制见 Docs/Performance/2026-09-28.md。本次本地交付未改变远程发布治理规则。
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 接入示例
|
||||
|
||||
这些示例直接引用仓库中参与编译和测试的实现,不另存代码副本。
|
||||
|
||||
| 目标 | 实现入口 | 可运行验证 |
|
||||
|---|---|---|
|
||||
| Context 组件,声明依赖并注册可逆状态 | [测试组件](../Assets/Modules/ShrinkContext.Core/Tests/TestComponents.cs) | [Fiber 生命周期](../Assets/Modules/ShrinkContext.Core/Tests/FiberLifecycleTests.cs)、[退役保留](../Assets/Modules/ShrinkContext.Core/Tests/RetirementRetentionTests.cs) |
|
||||
| Context 持有事件订阅并在卸载时撤回 | [EffectAttach](../Assets/Modules/ShrinkContext.EventBusAdapter/Runtime/ShrinkEventBusEffects.cs) | [事件接收和自动退订](../Assets/Modules/ShrinkContext.EventBusAdapter/Tests/EventBusEffectTests.cs) |
|
||||
| RPC 到 Command 桥接 | [桥注册和执行](../Assets/Modules/ShrinkCommand.Integration.Network/ShrinkNetworkCommandBridge.cs) | [双 provider 就绪/退役/替换](../Assets/Modules/ShrinkCommand.Integration.Network/Tests/IntegrationComponentsTests.cs) |
|
||||
|
||||
Unity Test Runner 的 EditMode 选择 `ShrinkContext.Core.Tests`、`ShrinkContext.EventBusAdapter.Tests`、`ShrinkCommand.Integration.Network.Tests`。Player 验证入口为 `Assets/AgentSupportTests/Editor/PlayerAcceptance.cs`,仅使用测试场景。独立共享运行时测试使用 `dotnet test Tools/AgentSupport/RuntimeTests`。
|
||||
|
||||
事件特性需要 CodeGen 完成实际绑定;静态索引里的订阅边只是候选。RPC 桥的权限由 `ShrinkNetworkCommandBridgeOptions` 和命令源共同决定,不能为了复用绕过原有权限。拥有者在组件退出时撤回它的 effect,不替他人释放借来的服务。
|
||||
@@ -0,0 +1,60 @@
|
||||
# Network v2 与诊断接入迁移
|
||||
|
||||
网络消息体的编码器与包封装现已分离。所有 `ShrinkNetworkService` 使用 SHK2 二进制封装;协议版本为 2,旧 JSON/MessagePack 外层包会被拒绝。客户端与服务端必须一起升级。直接使用 TCP/KCP transport 传递自定义字节的项目保留自己的上层协议,不能把其版本号与 ShrinkNetworkService 的协议号混同。
|
||||
|
||||
## 消息编码与所有权
|
||||
|
||||
- JSON 消息编码仍可显式选择 `ShrinkJsonNetworkSerializer`,不再将字节消息体嵌入 JSON 外层包而产生 Base64。
|
||||
- 原反射式 `ShrinkMessagePackNetworkSerializer` 已移除。安装可选的 `ShrinkSDK.Network.MessagePack`/UPM 适配包,引用 `ShrinkNetwork.MessagePack`,传入生成的 resolver,并对每个消息类型调用 `Register<T>()`。适配器只补充基础类型 formatter,不使用动态泛型反射回退。
|
||||
- `ShrinkNetworkPacket.Payload` 从 `byte[]` 改为 `ReadOnlyMemory<byte>`。只有需要独立所有权时调用 `ToArray()`;`ShrinkPacketCodec.Decode` 借用输入内存。
|
||||
- 实现 `IShrinkNetworkBufferSerializer` 可直接写入 `IBufferWriter<byte>`。旧消息序列化接口保留兼容路径,但会产生中间数组。
|
||||
- 实现 `IShrinkNetworkMemoryTransport` 时,发送任务完成、取消或失败之前必须保持输入有效;任务结束后不能继续访问。服务层负责在 await 结束后归还池内存。旧传输接口收到独立数组。
|
||||
- 广播可先编码消息体并使用 `SendSerializedAsync`,外层会话信息仍按收件人生成。调用方在所有发送完成前不得修改共享消息体。
|
||||
|
||||
## 排队行为
|
||||
|
||||
默认自动派发按序执行处理器;Unity/Godot 的主线程接入可使用 `ShrinkNetworkDispatchQueue`,按帧调用带数量、字节、时间预算的 `PumpAsync`。预算不会抢占正在执行的 handler;为避免饥饿,每轮允许首个合法包超过本轮字节预算。
|
||||
|
||||
可靠接收队列只允许 Reject。原 DropNewest/DropOldest 配置会明确报错。拒绝通过 `OnDispatchRejected` 与指标反馈;支持会话控制的 transport 会断开连接,以免继续使用缺失可靠操作的状态。
|
||||
|
||||
显式状态合并使用 `ShrinkNetworkWorkQueue.EnqueueAsync` 的 `stateKey`,只在相同 session/channel/key 内替换未执行项,替换后的项移至该分区队尾。返回值区分 Completed、Rejected、Replaced、Canceled;异常继续向调用方传播。RPC、可靠业务操作、存档事务和快照分片不能使用普通状态替换。多个分区轮转,但处理器仍串行执行。
|
||||
|
||||
RPC 响应单独完成挂起请求,避免嵌套调用等待当前处理队列而死锁。处理器的 async continuation 线程仍取决于其等待对象;访问引擎对象前由宿主保证线程切换。
|
||||
|
||||
## Context 与存档
|
||||
|
||||
`ShrinkContextRuntime.Fibers` 只保留仍被运行时管理的 fiber。退役历史改为有界纯数据诊断,构造参数 `retiredHistoryCapacity` 默认 64;需要审计时由上层保存诊断数据,不能继续依赖运行时永久持有所有历史组件。
|
||||
|
||||
生命周期转换使用同步队列展开深依赖链,保留确定性协调和依赖排空后撤回自身效应的规则。不要从多个线程同时修改同一 Context runtime。
|
||||
|
||||
`SaveOptions.EncodeInBackground` 默认关闭。开启后,模块采集和 JSON 快照物化仍在调用线程执行,后台只编码独立快照与加密。`CollectTimings` 提供采集、编码、加密、落盘阶段数据;存档格式、原子替换和备份回退保持兼容。
|
||||
|
||||
## 从包源升级
|
||||
|
||||
Unity 在 `Packages/manifest.json` 配置 ShrinkSDK scoped registry,使用明确版本号安装:
|
||||
|
||||
```json
|
||||
{
|
||||
"scopedRegistries": [{
|
||||
"name": "ShrinkSDK",
|
||||
"url": "https://git.crash.work/api/packages/ShrinkSDK/npm/",
|
||||
"scopes": ["com.cneicy"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
保留工程已有的 registry 和第三方依赖。核心版本为 Context 0.3.0、EventBus 2.2.2、Network 0.4.2、DataSaver 2.4.0、UPM CodeGen 0.2.1;按需添加 `com.cneicy.shrink-inspection` 0.1.0 与 `com.cneicy.shrink-network-messagepack` 0.1.0。完整版本以仓库版本目录及 [Gitea 包列表](https://git.crash.work/ShrinkSDK/-/packages) 为准。
|
||||
|
||||
.NET 使用 `https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json` 作为 NuGet 源。CLI 安装见 [工具说明](../Tools/AgentSupport/README.md)。
|
||||
|
||||
MessagePack 适配包要求消费项目提供 MessagePack-CSharp 与生成式 formatter,避免重复安装二进制。适配器使用 .NET 3.1.8,已验证 Unity 3.1.10 生成 formatter 的字节互通。
|
||||
|
||||
升级后由 Unity Package Manager 解析依赖,等待编译完成并确认包来源为 registry。导出 compilation inputs 后,再从实际宿主导出 runtime snapshot;未注册宿主的实例不在默认快照覆盖内。编译、消息互通和实际启动应分别验证。
|
||||
|
||||
修改 SDK 源码时,可使用 `Pack-Local.ps1` 和 `Install-Unity.py <工程路径>` 测试本地构建;这会把目标项目的 ShrinkSDK 依赖改为本地 tarball,正式消费项目应恢复 registry 版本。
|
||||
|
||||
### 回退
|
||||
|
||||
1. 停止 Play Mode,保存当前 manifest、lock 与相关接入代码。
|
||||
2. 同步回退使用新版 API 的业务接入与依赖版本,由 Unity 重建 lock;不手工修改 Library。
|
||||
3. 客户端和使用 ShrinkNetworkService 的服务端共同回退。v1/v2 不兼容,不可只回退单边;自定义 transport 协议按其独立策略处理。
|
||||
@@ -0,0 +1,80 @@
|
||||
# 性能测量 — 2026-09-28 UTC
|
||||
|
||||
测量环境为本地开发机;结果用于比较实现,不代表生产容量。原始测量时间保留在 JSON 中。
|
||||
|
||||
## 方法与证据边界
|
||||
|
||||
- Windows x64;Unity 2022.3.62f3 的实际 Standalone Player,而非用 EditMode 冒充 Player;另有 .NET 8.0.31、.NET 10 和 Godot 4.6.3 Mono 的实际运行。
|
||||
- 网络矩阵:64 B/1 KB/32 KB 负载参数,1/32/256 会话;RPC、广播、慢消费者、突发队列。Loopback 会话不是 OS socket;TCP 文件是实际本机 socket,且 RPC 采用串行往返采样,不等于高并发容量压测。
|
||||
- Context:10/100/1000 组件依赖链,10,000 次创建退役;旧实现来自 Context 子模块提交 b93f3e61223e7572588b67c0d3935073817f0da6,不是重新编写的假想基线。
|
||||
- 存档:100 KB/1 MB/10 MB,前台/后台编码;.NET 每组合 10 次并覆盖加密,Player 记录真实阶段耗时与采集线程。输入主要为字符串快照,不能推断复杂游戏对象的采集成本。
|
||||
- 单机开发环境、有限样本,非隔离实验机;p99 易受 JIT、GC、系统调度影响。没有可靠的多轮置信区间,不将小幅波动称为普遍提升。
|
||||
- Unity 的 GC.GetAllocatedBytesForCurrentThread 对已知分配返回零,已弃用该结果。Player 改用经已知数组分配校准的 GC.Alloc 事件计数;事件次数不是 B/op。独立 .NET 的字节计数与 Player 分开报告。
|
||||
|
||||
原始数据位于 [Data/2026-09-28](Data/2026-09-28/);Player 测量输出保存在 player-output.txt。工具、测试源代码保留在 Tools/AgentSupport 与 Assets/AgentSupportTests。
|
||||
|
||||
## 已量化的核心变化
|
||||
|
||||
### Context(.NET 8)
|
||||
|
||||
| 组件数 | 旧诊断 p95 µs | 新诊断 p95 µs | 旧 B/op | 新 B/op |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 10 | 9.4 | 11.7 | 10,456 | 7,312 |
|
||||
| 100 | 538.2 | 116.9 | 108,016 | 73,456 |
|
||||
| 1000 | 7,426.4 | 548.3 | 1,083,616 | 735,328 |
|
||||
|
||||
大规模诊断通过按依赖键访问 provider 候选索引获益;10 个组件的 p95 本轮反而略高,不能宣称所有规模都更快。10,000 次退役后旧运行时持有 10,000 个 fiber,新运行时为 0,诊断历史有界 64。弱引用测试验证实例可回收;整进程 GC 差值可能为负,不将其解读为精确节省字节数。
|
||||
|
||||
深链生命周期改为迭代队列展开,避免 1000 层同步完成/撤回造成栈溢出;不改变依赖排空与效应撤回顺序。
|
||||
|
||||
### 网络封装(.NET 8)
|
||||
|
||||
| 负载参数 | 旧 JSON 外封装 B/op | 新 v2 外封装 B/op |
|
||||
|---:|---:|---:|
|
||||
| 64 B | 3,504 | 32 |
|
||||
| 1 KB | 10,560 | 32 |
|
||||
| 32 KB | 230,328 | 32 |
|
||||
|
||||
这里只比较已经编码的消息体如何封装,不是端到端零分配。新封装取消外层 JSON/Base64,消息直接写入池化 buffer;仍有 writer 对象、消息反序列化、排队和 transport 的独立成本。JSON 与生成式 MessagePack 的完整消息编码对照见原始 rows,不能套用此表的 32 B。
|
||||
|
||||
队列突发测试输入 10,000 个显式可替换状态,32 个 session 分区最终保留 32 项、32 KB,返回 9,968 次 Replaced;一次 4 KB budget 泵送完成 4 项。可靠操作不使用这种替换机制,满队列显式拒绝。
|
||||
|
||||
### 实际 TCP 与后端选择
|
||||
|
||||
256 个本机 TCP 连接、1 KB、256 次串行 RPC 往返:
|
||||
|
||||
| 运行时 | p50 µs | p95 µs | p99 µs | ops/s |
|
||||
|---|---:|---:|---:|---:|
|
||||
| .NET 8 | 99.3 | 258.9 | 355.1 | 6,911 |
|
||||
| .NET 10 | 88.4 | 250.8 | 384.3 | 7,392 |
|
||||
|
||||
其他参数组合有升有降;本轮未证明目标场景稳定达到 15% 收益,因此不引入新的默认 .NET 10 专属服务端依赖。Pipelines 仅有内存 buffer roundtrip 微实验,未实现等价 socket 后端对照,不足以采纳。共享运行时继续 netstandard2.1。
|
||||
|
||||
TCP 慢消费者测试证明有界队列和拒绝反馈,不证明业务消费成功:该突发负载使用 RPC 消息的一次性发送路径,记录中存在 DispatchMissCount;不能将该行视为完整 handler 吞吐测试。可靠队列行为、会话隔离、取消和故障另由功能测试验证。KCP 未作独立吞吐结论。
|
||||
|
||||
### EventBus 与存档
|
||||
|
||||
.NET 8 同步 Post、同步完成的异步 Post 为 0 B/op;注册/撤回 224 B/op,按需绑定诊断 120 B/op。同步路径的捕获闭包已移出快路径;Unity 的结构体事件空值检查另外增加值类型短路,避免 Mono 装箱。
|
||||
|
||||
校准后的 Player 实测:独立 Bus 10,000 次同步发布为 0 次分配事件;当前默认 Bus 为 10,000 次分配事件。修复值类型空值检查前默认通道是 20,000 次,不能将早先字节 API 的零值作为基线。
|
||||
|
||||
默认全局 Bus 可能带网络桥或诊断观察器;接口观察器需要结构体装箱,不能把独立 Bus 的零分配结果扩大到所有配置。Player 同时测量默认与独立通道,并为引用类型空值保留异常检查。
|
||||
|
||||
存档采集仍在调用线程,后台开关仅移动脱离宿主对象的快照编码与加密。示例 .NET 8 的 10 MB 未加密平均编码为前台约 7.68 ms、后台约 8.21 ms;主要收益是主线程工作转移,而非宣称总保存时间必然缩短。Player 的冷启动与各阶段实测见 player-output.txt;未隐藏首轮高耗时。格式、原子落盘、取消时备份保留及损坏回退均有测试。
|
||||
|
||||
## 复现
|
||||
|
||||
在已还原依赖的仓库运行:
|
||||
|
||||
~~~powershell
|
||||
pwsh Tools/AgentSupport/Benchmarks/Prepare-Baseline.ps1
|
||||
dotnet run --project Tools/AgentSupport/Benchmarks -c Release -f net8.0 -- Artifacts/AgentSupport/benchmark-net8.json
|
||||
dotnet run --project Tools/AgentSupport/Benchmarks -c Release -f net10.0 -- Artifacts/AgentSupport/benchmark-net10.json
|
||||
dotnet run --project Tools/AgentSupport/Benchmarks -c Release -f net8.0 -- --tcp Artifacts/AgentSupport/tcp-net8.json
|
||||
dotnet test Tools/AgentSupport/RuntimeTests
|
||||
dotnet test Tools/AgentSupport/Shrink.Inspect.Tests
|
||||
~~~
|
||||
|
||||
Unity 用 TestRunner 的 StandaloneWindows64 Player 运行 ShrinkSDK.AgentSupport.PlayerTests;Editor 辅助入口 PlayerAcceptance.Run() 将本次结果保存到 Artifacts/AgentSupport/unity-player-results.xml。基线脚本默认固定上述旧提交;使用 `-Revision` 可指定其他对照版本。
|
||||
|
||||
协议和回退见 [迁移说明](../NetworkV2Migration.md)。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 性能测量原始数据
|
||||
|
||||
- `benchmark-net8.json`、`benchmark-net10.json`:网络编码、队列、Context、EventBus 和存档微基准。
|
||||
- `tcp-net8.json`、`tcp-net10.json`:真实本机 TCP socket 测量。
|
||||
- `player-output.txt`:Unity Standalone Player 的阶段计时与分配事件计数。
|
||||
|
||||
环境、采样限制、结果解释和复现命令见 [性能报告](../../2026-09-28.md)。
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
PLAYER Context 10000 retirements ms=123.6386
|
||||
|
||||
PLAYER EventBus 10000 posts allocationEvents=10000 ms=0.8038
|
||||
PLAYER EventBus isolated 10000 posts allocationEvents=0
|
||||
|
||||
[ShrinkDataSaver] 槽位 0 已保存。(2 个模块)
|
||||
PLAYER Save 102400 background=False captureMs=11.8073 encodeMs=7.7169 encryptMs=0 writeMs=17.803 bytes=103171
|
||||
[ShrinkDataSaver] 槽位 0 已保存。(2 个模块)
|
||||
PLAYER Save 102400 background=True captureMs=0.1186 encodeMs=0.2584 encryptMs=0 writeMs=29.4953 bytes=103171
|
||||
[ShrinkDataSaver] 槽位 0 已保存。(2 个模块)
|
||||
PLAYER Save 1048576 background=False captureMs=1.3102 encodeMs=7.8701 encryptMs=0 writeMs=13.9663 bytes=1049347
|
||||
[ShrinkDataSaver] 槽位 0 已保存。(2 个模块)
|
||||
PLAYER Save 1048576 background=True captureMs=0.1341 encodeMs=7.7226 encryptMs=0 writeMs=23.1749 bytes=1049347
|
||||
[ShrinkDataSaver] 槽位 0 已保存。(2 个模块)
|
||||
PLAYER Save 10485760 background=False captureMs=0.1424 encodeMs=28.173 encryptMs=0 writeMs=32.9821 bytes=10486531
|
||||
[ShrinkDataSaver] 槽位 0 已保存。(2 个模块)
|
||||
PLAYER Save 10485760 background=True captureMs=0.162 encodeMs=25.6417 encryptMs=0 writeMs=24.231 bytes=10486531
|
||||
|
||||
PLAYER allocation calibration managedCounterDelta=0 profilerEvents=10; use profiler events, not uncalibrated bytes.
|
||||
|
||||
PLAYER PERF context-diagnostic/components=10 n=100 ops=71994.24 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=135.00 p50us=13.00 p95us=20.20 p99us=25.60
|
||||
PLAYER PERF context-diagnostic/components=100 n=100 ops=6276.68 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1224.00 p50us=118.80 p95us=190.80 p99us=1980.20
|
||||
PLAYER PERF context-diagnostic/components=1000 n=100 ops=391.93 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=12033.00 p50us=1841.90 p95us=5669.60 p99us=7090.60
|
||||
PLAYER PERF event-async-completed n=10000 ops=1531534.29 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=3.00 p50us=0.30 p95us=0.60 p99us=1.10
|
||||
PLAYER PERF event-bindings n=1000 ops=3377237.42 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=2.00 p50us=0.20 p95us=0.60 p99us=1.00
|
||||
PLAYER PERF event-register-withdraw n=10000 ops=725100.06 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=10.00 p50us=0.80 p95us=1.70 p99us=2.40
|
||||
|
||||
PLAYER PERF loopback-rpc/bytes=64/sessions=1 n=128 ops=42139.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=22.40 p95us=32.20 p99us=35.80
|
||||
PLAYER PERF loopback-broadcast/bytes=64/sessions=1 n=20 ops=135777.33 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=37.00 p50us=7.20 p95us=9.20 p99us=9.20
|
||||
PLAYER PERF loopback-rpc/bytes=64/sessions=32 n=128 ops=22296.16 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=24.60 p95us=46.10 p99us=96.20
|
||||
PLAYER PERF loopback-broadcast/bytes=64/sessions=32 n=20 ops=3283.21 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1060.00 p50us=215.80 p95us=1999.10 p99us=1999.10
|
||||
PLAYER PERF loopback-rpc/bytes=64/sessions=256 n=128 ops=16764.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=100.00 p50us=33.90 p95us=42.50 p99us=115.00
|
||||
PLAYER PERF loopback-broadcast/bytes=64/sessions=256 n=20 ops=331.02 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8452.00 p50us=3052.70 p95us=5858.40 p99us=5858.40
|
||||
PLAYER PERF loopback-rpc/bytes=1024/sessions=1 n=128 ops=13102.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.70 p95us=302.40 p99us=1201.80
|
||||
PLAYER PERF loopback-broadcast/bytes=1024/sessions=1 n=20 ops=117164.62 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=38.00 p50us=8.40 p95us=10.40 p99us=10.40
|
||||
PLAYER PERF loopback-rpc/bytes=1024/sessions=32 n=128 ops=15497.49 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=27.60 p95us=62.90 p99us=1200.70
|
||||
PLAYER PERF loopback-broadcast/bytes=1024/sessions=32 n=20 ops=1648.17 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1092.00 p50us=361.60 p95us=1577.50 p99us=1577.50
|
||||
PLAYER PERF loopback-rpc/bytes=1024/sessions=256 n=128 ops=12226.92 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=102.00 p50us=29.00 p95us=305.10 p99us=1402.90
|
||||
PLAYER PERF loopback-broadcast/bytes=1024/sessions=256 n=20 ops=194.15 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=8708.00 p50us=4612.30 p95us=10631.00 p99us=10631.00
|
||||
PLAYER PERF loopback-rpc/bytes=32768/sessions=1 n=128 ops=828.26 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1123.40 p95us=3500.10 p99us=3837.10
|
||||
PLAYER PERF loopback-broadcast/bytes=32768/sessions=1 n=20 ops=1815.71 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=43.00 p50us=98.60 p95us=3193.40 p99us=3193.40
|
||||
PLAYER PERF loopback-rpc/bytes=32768/sessions=32 n=128 ops=842.40 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.00 p50us=1163.90 p95us=3475.30 p99us=3500.30
|
||||
PLAYER PERF loopback-broadcast/bytes=32768/sessions=32 n=20 ops=65.08 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=1252.90 p50us=15096.30 p95us=26400.40 p99us=26400.40
|
||||
PLAYER PERF loopback-rpc/bytes=32768/sessions=256 n=128 ops=830.36 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=112.60 p50us=1230.40 p95us=3500.80 p99us=3549.90
|
||||
PLAYER PERF loopback-broadcast/bytes=32768/sessions=256 n=20 ops=8.67 allocBytes=unsupported allocationSamples=10 allocEventsPerOp=9995.60 p50us=114364.50 p95us=124433.00 p99us=124433.00
|
||||
|
||||
PLAYER Queue pending=32 bytes=32768 replaced=96 rejected=0
|
||||
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"runtime": ".NET 10.0.8",
|
||||
"host": "Windows loopback TCP; sequential round-trip samples, multiple real connections",
|
||||
"rows": [
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 1,
|
||||
"bytes": 64,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 2569.8786133898707,
|
||||
"p50Microseconds": 59.1,
|
||||
"p95Microseconds": 172,
|
||||
"p99Microseconds": 302.8,
|
||||
"processAllocatedBytes": 4181192
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 1,
|
||||
"bytes": 64,
|
||||
"elapsedMilliseconds": 3.9542,
|
||||
"delivered": 1
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 1,
|
||||
"bytes": 1024,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 9339.622985687758,
|
||||
"p50Microseconds": 76.8,
|
||||
"p95Microseconds": 233.8,
|
||||
"p99Microseconds": 413.7,
|
||||
"processAllocatedBytes": 9931824
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 1,
|
||||
"bytes": 1024,
|
||||
"elapsedMilliseconds": 4.1822,
|
||||
"delivered": 1
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 1,
|
||||
"bytes": 32768,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 2077.1380931710028,
|
||||
"p50Microseconds": 403.3,
|
||||
"p95Microseconds": 1068.7,
|
||||
"p99Microseconds": 1287.4,
|
||||
"processAllocatedBytes": 229642408
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 1,
|
||||
"bytes": 32768,
|
||||
"elapsedMilliseconds": 0.8935,
|
||||
"delivered": 1
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/slow-consumer-burst",
|
||||
"connections": 1,
|
||||
"attempts": 8,
|
||||
"beforePump": {
|
||||
"PendingCount": 2,
|
||||
"PendingBytes": 2136,
|
||||
"Rejected": 1,
|
||||
"Replaced": 0,
|
||||
"Completed": 0,
|
||||
"OldestWaitMilliseconds": 108.5865,
|
||||
"LastWaitMilliseconds": 0
|
||||
},
|
||||
"afterPump": {
|
||||
"PendingCount": 0,
|
||||
"PendingBytes": 0,
|
||||
"Rejected": 1,
|
||||
"Replaced": 0,
|
||||
"Completed": 2,
|
||||
"OldestWaitMilliseconds": 0,
|
||||
"LastWaitMilliseconds": 108.3473
|
||||
},
|
||||
"metrics": {
|
||||
"CurrentSessions": 1,
|
||||
"SessionsConnected": 1,
|
||||
"SessionsDisconnected": 0,
|
||||
"PacketsSent": 771,
|
||||
"PacketsReceived": 770,
|
||||
"BytesSent": 8772548,
|
||||
"BytesReceived": 8703064,
|
||||
"RpcStarted": 0,
|
||||
"RpcCompleted": 0,
|
||||
"RpcTimedOut": 0,
|
||||
"RpcCanceled": 0,
|
||||
"RpcFailed": 0,
|
||||
"ProtocolViolations": 0,
|
||||
"AuthRejectedCount": 0,
|
||||
"PermissionDeniedCount": 0,
|
||||
"HandlerExceptionCount": 0,
|
||||
"UnknownOpcodeCount": 0,
|
||||
"DispatchMissCount": 2,
|
||||
"SerializationErrorCount": 0,
|
||||
"DispatchQueueRejectedCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 32,
|
||||
"bytes": 64,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 13928.182807399347,
|
||||
"p50Microseconds": 58.6,
|
||||
"p95Microseconds": 147.2,
|
||||
"p99Microseconds": 235,
|
||||
"processAllocatedBytes": 3788328
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 32,
|
||||
"bytes": 64,
|
||||
"elapsedMilliseconds": 15.9674,
|
||||
"delivered": 32
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 32,
|
||||
"bytes": 1024,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 9348.968509314276,
|
||||
"p50Microseconds": 79.3,
|
||||
"p95Microseconds": 199.6,
|
||||
"p99Microseconds": 551,
|
||||
"processAllocatedBytes": 9913976
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 32,
|
||||
"bytes": 1024,
|
||||
"elapsedMilliseconds": 3.5947,
|
||||
"delivered": 32
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 32,
|
||||
"bytes": 32768,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 1782.1179776023969,
|
||||
"p50Microseconds": 446.9,
|
||||
"p95Microseconds": 1466.1,
|
||||
"p99Microseconds": 1839.8,
|
||||
"processAllocatedBytes": 229509592
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 32,
|
||||
"bytes": 32768,
|
||||
"elapsedMilliseconds": 13.9419,
|
||||
"delivered": 32
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/slow-consumer-burst",
|
||||
"connections": 32,
|
||||
"attempts": 256,
|
||||
"beforePump": {
|
||||
"PendingCount": 30,
|
||||
"PendingBytes": 32040,
|
||||
"Rejected": 32,
|
||||
"Replaced": 0,
|
||||
"Completed": 0,
|
||||
"OldestWaitMilliseconds": 109.5715,
|
||||
"LastWaitMilliseconds": 0
|
||||
},
|
||||
"afterPump": {
|
||||
"PendingCount": 23,
|
||||
"PendingBytes": 24564,
|
||||
"Rejected": 32,
|
||||
"Replaced": 0,
|
||||
"Completed": 7,
|
||||
"OldestWaitMilliseconds": 109.1555,
|
||||
"LastWaitMilliseconds": 109.1537
|
||||
},
|
||||
"metrics": {
|
||||
"CurrentSessions": 32,
|
||||
"SessionsConnected": 32,
|
||||
"SessionsDisconnected": 0,
|
||||
"PacketsSent": 864,
|
||||
"PacketsReceived": 775,
|
||||
"BytesSent": 9826176,
|
||||
"BytesReceived": 8708404,
|
||||
"RpcStarted": 0,
|
||||
"RpcCompleted": 0,
|
||||
"RpcTimedOut": 0,
|
||||
"RpcCanceled": 0,
|
||||
"RpcFailed": 0,
|
||||
"ProtocolViolations": 0,
|
||||
"AuthRejectedCount": 0,
|
||||
"PermissionDeniedCount": 0,
|
||||
"HandlerExceptionCount": 0,
|
||||
"UnknownOpcodeCount": 0,
|
||||
"DispatchMissCount": 7,
|
||||
"SerializationErrorCount": 0,
|
||||
"DispatchQueueRejectedCount": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 256,
|
||||
"bytes": 64,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 8559.783062997996,
|
||||
"p50Microseconds": 74.8,
|
||||
"p95Microseconds": 204,
|
||||
"p99Microseconds": 313.2,
|
||||
"processAllocatedBytes": 4045992
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 256,
|
||||
"bytes": 64,
|
||||
"elapsedMilliseconds": 12.374,
|
||||
"delivered": 256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 256,
|
||||
"bytes": 1024,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 7391.815297015266,
|
||||
"p50Microseconds": 88.4,
|
||||
"p95Microseconds": 250.8,
|
||||
"p99Microseconds": 384.3,
|
||||
"processAllocatedBytes": 9917520
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 256,
|
||||
"bytes": 1024,
|
||||
"elapsedMilliseconds": 12.6405,
|
||||
"delivered": 256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 256,
|
||||
"bytes": 32768,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 1195.502482302127,
|
||||
"p50Microseconds": 418.6,
|
||||
"p95Microseconds": 1033.3,
|
||||
"p99Microseconds": 16401.9,
|
||||
"processAllocatedBytes": 227807112
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 256,
|
||||
"bytes": 32768,
|
||||
"elapsedMilliseconds": 47.4633,
|
||||
"delivered": 256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/slow-consumer-burst",
|
||||
"connections": 256,
|
||||
"attempts": 2048,
|
||||
"beforePump": {
|
||||
"PendingCount": 30,
|
||||
"PendingBytes": 32040,
|
||||
"Rejected": 256,
|
||||
"Replaced": 0,
|
||||
"Completed": 0,
|
||||
"OldestWaitMilliseconds": 124.5254,
|
||||
"LastWaitMilliseconds": 0
|
||||
},
|
||||
"afterPump": {
|
||||
"PendingCount": 23,
|
||||
"PendingBytes": 24564,
|
||||
"Rejected": 256,
|
||||
"Replaced": 0,
|
||||
"Completed": 7,
|
||||
"OldestWaitMilliseconds": 124.4757,
|
||||
"LastWaitMilliseconds": 124.5044
|
||||
},
|
||||
"metrics": {
|
||||
"CurrentSessions": 256,
|
||||
"SessionsConnected": 256,
|
||||
"SessionsDisconnected": 0,
|
||||
"PacketsSent": 1536,
|
||||
"PacketsReceived": 775,
|
||||
"BytesSent": 17439488,
|
||||
"BytesReceived": 8708404,
|
||||
"RpcStarted": 0,
|
||||
"RpcCompleted": 0,
|
||||
"RpcTimedOut": 0,
|
||||
"RpcCanceled": 0,
|
||||
"RpcFailed": 0,
|
||||
"ProtocolViolations": 0,
|
||||
"AuthRejectedCount": 0,
|
||||
"PermissionDeniedCount": 0,
|
||||
"HandlerExceptionCount": 0,
|
||||
"UnknownOpcodeCount": 0,
|
||||
"DispatchMissCount": 7,
|
||||
"SerializationErrorCount": 0,
|
||||
"DispatchQueueRejectedCount": 256
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"runtime": ".NET 8.0.31",
|
||||
"host": "Windows loopback TCP; sequential round-trip samples, multiple real connections",
|
||||
"rows": [
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 1,
|
||||
"bytes": 64,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 2786.3308702603804,
|
||||
"p50Microseconds": 61.2,
|
||||
"p95Microseconds": 164.7,
|
||||
"p99Microseconds": 395.7,
|
||||
"processAllocatedBytes": 4198840
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 1,
|
||||
"bytes": 64,
|
||||
"elapsedMilliseconds": 2.3583,
|
||||
"delivered": 1
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 1,
|
||||
"bytes": 1024,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 9602.940900650823,
|
||||
"p50Microseconds": 75.7,
|
||||
"p95Microseconds": 193.2,
|
||||
"p99Microseconds": 496,
|
||||
"processAllocatedBytes": 9938800
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 1,
|
||||
"bytes": 1024,
|
||||
"elapsedMilliseconds": 4.7543,
|
||||
"delivered": 1
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 1,
|
||||
"bytes": 32768,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 2206.3658826885603,
|
||||
"p50Microseconds": 384.2,
|
||||
"p95Microseconds": 1002.5,
|
||||
"p99Microseconds": 1126.9,
|
||||
"processAllocatedBytes": 229867960
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 1,
|
||||
"bytes": 32768,
|
||||
"elapsedMilliseconds": 10.4461,
|
||||
"delivered": 1
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/slow-consumer-burst",
|
||||
"connections": 1,
|
||||
"attempts": 8,
|
||||
"beforePump": {
|
||||
"PendingCount": 2,
|
||||
"PendingBytes": 2136,
|
||||
"Rejected": 1,
|
||||
"Replaced": 0,
|
||||
"Completed": 0,
|
||||
"OldestWaitMilliseconds": 109.4399,
|
||||
"LastWaitMilliseconds": 0
|
||||
},
|
||||
"afterPump": {
|
||||
"PendingCount": 0,
|
||||
"PendingBytes": 0,
|
||||
"Rejected": 1,
|
||||
"Replaced": 0,
|
||||
"Completed": 2,
|
||||
"OldestWaitMilliseconds": 0,
|
||||
"LastWaitMilliseconds": 109.0027
|
||||
},
|
||||
"metrics": {
|
||||
"CurrentSessions": 1,
|
||||
"SessionsConnected": 1,
|
||||
"SessionsDisconnected": 0,
|
||||
"PacketsSent": 771,
|
||||
"PacketsReceived": 770,
|
||||
"BytesSent": 8772548,
|
||||
"BytesReceived": 8703064,
|
||||
"RpcStarted": 0,
|
||||
"RpcCompleted": 0,
|
||||
"RpcTimedOut": 0,
|
||||
"RpcCanceled": 0,
|
||||
"RpcFailed": 0,
|
||||
"ProtocolViolations": 0,
|
||||
"AuthRejectedCount": 0,
|
||||
"PermissionDeniedCount": 0,
|
||||
"HandlerExceptionCount": 0,
|
||||
"UnknownOpcodeCount": 0,
|
||||
"DispatchMissCount": 2,
|
||||
"SerializationErrorCount": 0,
|
||||
"DispatchQueueRejectedCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 32,
|
||||
"bytes": 64,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 9747.14534288249,
|
||||
"p50Microseconds": 78.5,
|
||||
"p95Microseconds": 235.1,
|
||||
"p99Microseconds": 304.2,
|
||||
"processAllocatedBytes": 3791256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 32,
|
||||
"bytes": 64,
|
||||
"elapsedMilliseconds": 9.6954,
|
||||
"delivered": 32
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 32,
|
||||
"bytes": 1024,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 8234.052955253068,
|
||||
"p50Microseconds": 86.6,
|
||||
"p95Microseconds": 247.5,
|
||||
"p99Microseconds": 494.8,
|
||||
"processAllocatedBytes": 9908800
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 32,
|
||||
"bytes": 1024,
|
||||
"elapsedMilliseconds": 15.3001,
|
||||
"delivered": 32
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 32,
|
||||
"bytes": 32768,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 1754.0157368099387,
|
||||
"p50Microseconds": 419.6,
|
||||
"p95Microseconds": 1319.5,
|
||||
"p99Microseconds": 1647.1,
|
||||
"processAllocatedBytes": 230263696
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 32,
|
||||
"bytes": 32768,
|
||||
"elapsedMilliseconds": 11.5336,
|
||||
"delivered": 32
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/slow-consumer-burst",
|
||||
"connections": 32,
|
||||
"attempts": 256,
|
||||
"beforePump": {
|
||||
"PendingCount": 30,
|
||||
"PendingBytes": 32040,
|
||||
"Rejected": 32,
|
||||
"Replaced": 0,
|
||||
"Completed": 0,
|
||||
"OldestWaitMilliseconds": 126.3722,
|
||||
"LastWaitMilliseconds": 0
|
||||
},
|
||||
"afterPump": {
|
||||
"PendingCount": 23,
|
||||
"PendingBytes": 24564,
|
||||
"Rejected": 32,
|
||||
"Replaced": 0,
|
||||
"Completed": 7,
|
||||
"OldestWaitMilliseconds": 126.2021,
|
||||
"LastWaitMilliseconds": 126.1876
|
||||
},
|
||||
"metrics": {
|
||||
"CurrentSessions": 32,
|
||||
"SessionsConnected": 32,
|
||||
"SessionsDisconnected": 0,
|
||||
"PacketsSent": 864,
|
||||
"PacketsReceived": 775,
|
||||
"BytesSent": 9826176,
|
||||
"BytesReceived": 8708404,
|
||||
"RpcStarted": 0,
|
||||
"RpcCompleted": 0,
|
||||
"RpcTimedOut": 0,
|
||||
"RpcCanceled": 0,
|
||||
"RpcFailed": 0,
|
||||
"ProtocolViolations": 0,
|
||||
"AuthRejectedCount": 0,
|
||||
"PermissionDeniedCount": 0,
|
||||
"HandlerExceptionCount": 0,
|
||||
"UnknownOpcodeCount": 0,
|
||||
"DispatchMissCount": 7,
|
||||
"SerializationErrorCount": 0,
|
||||
"DispatchQueueRejectedCount": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 256,
|
||||
"bytes": 64,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 6808.96336192566,
|
||||
"p50Microseconds": 104.3,
|
||||
"p95Microseconds": 248.8,
|
||||
"p99Microseconds": 376.4,
|
||||
"processAllocatedBytes": 4047528
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 256,
|
||||
"bytes": 64,
|
||||
"elapsedMilliseconds": 8.4403,
|
||||
"delivered": 256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 256,
|
||||
"bytes": 1024,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 6911.465743698012,
|
||||
"p50Microseconds": 99.3,
|
||||
"p95Microseconds": 258.9,
|
||||
"p99Microseconds": 355.1,
|
||||
"processAllocatedBytes": 9916024
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 256,
|
||||
"bytes": 1024,
|
||||
"elapsedMilliseconds": 9.0517,
|
||||
"delivered": 256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/rpc",
|
||||
"connections": 256,
|
||||
"bytes": 32768,
|
||||
"operations": 256,
|
||||
"throughputPerSecond": 1056.106917624073,
|
||||
"p50Microseconds": 439.3,
|
||||
"p95Microseconds": 1988.3,
|
||||
"p99Microseconds": 14849.1,
|
||||
"processAllocatedBytes": 228731112
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/broadcast",
|
||||
"connections": 256,
|
||||
"bytes": 32768,
|
||||
"elapsedMilliseconds": 64.9767,
|
||||
"delivered": 256
|
||||
},
|
||||
{
|
||||
"scenario": "tcp/slow-consumer-burst",
|
||||
"connections": 256,
|
||||
"attempts": 2048,
|
||||
"beforePump": {
|
||||
"PendingCount": 30,
|
||||
"PendingBytes": 32040,
|
||||
"Rejected": 256,
|
||||
"Replaced": 0,
|
||||
"Completed": 0,
|
||||
"OldestWaitMilliseconds": 123.9167,
|
||||
"LastWaitMilliseconds": 0
|
||||
},
|
||||
"afterPump": {
|
||||
"PendingCount": 23,
|
||||
"PendingBytes": 24564,
|
||||
"Rejected": 256,
|
||||
"Replaced": 0,
|
||||
"Completed": 7,
|
||||
"OldestWaitMilliseconds": 123.3004,
|
||||
"LastWaitMilliseconds": 123.418
|
||||
},
|
||||
"metrics": {
|
||||
"CurrentSessions": 256,
|
||||
"SessionsConnected": 256,
|
||||
"SessionsDisconnected": 0,
|
||||
"PacketsSent": 1536,
|
||||
"PacketsReceived": 775,
|
||||
"BytesSent": 17439488,
|
||||
"BytesReceived": 8708404,
|
||||
"RpcStarted": 0,
|
||||
"RpcCompleted": 0,
|
||||
"RpcTimedOut": 0,
|
||||
"RpcCanceled": 0,
|
||||
"RpcFailed": 0,
|
||||
"ProtocolViolations": 0,
|
||||
"AuthRejectedCount": 0,
|
||||
"PermissionDeniedCount": 0,
|
||||
"HandlerExceptionCount": 0,
|
||||
"UnknownOpcodeCount": 0,
|
||||
"DispatchMissCount": 7,
|
||||
"SerializationErrorCount": 0,
|
||||
"DispatchQueueRejectedCount": 256
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# 新系统与玩法内容:先查可复用能力
|
||||
|
||||
仅在新增系统、玩法内容或扩展机制时使用本文。普通修复先定位受影响代码。
|
||||
|
||||
1. 用一句话明确目标行为和约束;不要增加未提出的玩法联动。
|
||||
2. 执行 `shrink inspect capabilities "关键词" --root <项目>`。先读最多三个候选的用途、限制、扩展点和证据,再查选中模块的 `symbol` / `flow`。中文使用项目业务词,例如 `存档`、`状态`、`属性`;不同叫法可另查别名。
|
||||
3. 按 **配置 → 组合 → 扩展已有机制 → 新模块** 判断。候选命中不是复用证明;对照真实接口、实例和行为测试。资料缺失不能解释成能力不存在。
|
||||
4. 确认状态归属、操作入口、结构化结果和退出时的撤销。只展开实际相关的持久化、联机、UI 或生命周期约束。
|
||||
5. 明确列出已存在、本次新增和待确认。不要为复用绕开权限、反向依赖模块、用无结果事件执行需要裁决的操作,或把大量胶水代码包装成零新增能力。
|
||||
|
||||
默认输出四项:复用方案及证据、必要改动、协作关系、影响与验证。只有会改变结果而无法从项目推导的选择才询问用户。
|
||||
|
||||
例如击杀怒气机制,需要分别核对击杀事实、计量、条件触发、属性修饰及效果撤回。存在 EventBus 不证明这些业务能力已经存在;存在存档功能也不意味着这次怒气必须持久化。
|
||||
|
||||
## 能力说明格式
|
||||
|
||||
在模块现有 README 中只保留一个区段。说明业务含义,不重复生成的 API 签名、依赖和版本。
|
||||
|
||||
```text
|
||||
<!-- shrink:capabilities -->
|
||||
Capability: 能实现的行为
|
||||
Aliases: 常见中文叫法 English aliases
|
||||
Limits: 明确不支持的行为或成立条件
|
||||
Extension: 可配置或组合的位置;需要新增代码的边界
|
||||
Evidence: 相对本 README 的源码、配置类型和可运行示例路径
|
||||
<!-- /shrink:capabilities -->
|
||||
```
|
||||
|
||||
框架维护 SDK 能力,消费项目维护自己的业务能力。业务目录即使共享 asmdef 也可以声明能力。不得假定所有项目都有背包、战斗、任务或属性系统。
|
||||
@@ -0,0 +1,7 @@
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/.godot/
|
||||
Artifacts/
|
||||
Exports/
|
||||
!*.csproj
|
||||
!*.sln
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Deterministic>true</Deterministic>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<RepositoryUrl>https://git.crash.work/ShrinkSDK/Workspace</RepositoryUrl>
|
||||
<Authors>ShrinkSDK</Authors>
|
||||
<Company>ShrinkSDK</Company>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="Godot Local" value="C:\Users\im\Documents\godothub\engine\4.6.3-stable-mono\GodotSharp\Tools\nupkgs" />
|
||||
<add key="ShrinkSDK Local" value="Artifacts" />
|
||||
<add key="ShrinkSDK" value="https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
Submodule
+1
Submodule Godot/Packages/ShrinkSDK.Godot added at e355bbfd35
+170
@@ -0,0 +1,170 @@
|
||||
# ShrinkSDK for Godot 4.6 C#
|
||||
|
||||
Godot 适配层使用共享 ShrinkSDK 运行时和 MSBuild/Cecil CodeGen。首期目标是 Godot 4.6.3 Mono、.NET 8,以及 Windows、Linux、macOS 桌面项目;不包含 Web、Android 和 iOS。
|
||||
|
||||
## 直接运行 Workspace 示例
|
||||
|
||||
使用 Godot 4.6.3 Mono 打开:
|
||||
|
||||
```text
|
||||
Godot/Samples/ShrinkSDK.Godot.Sample/project.godot
|
||||
```
|
||||
|
||||
首次打开前可以先完成导入和编译:
|
||||
|
||||
```powershell
|
||||
$godot = Join-Path $env:GODOT_HOME "godot_console.exe"
|
||||
& $godot --headless --editor --path .\Godot\Samples\ShrinkSDK.Godot.Sample --quit
|
||||
dotnet build .\Godot\Samples\ShrinkSDK.Godot.Sample\ShrinkSDK.Godot.Sample.csproj
|
||||
& $godot --headless --path .\Godot\Samples\ShrinkSDK.Godot.Sample
|
||||
```
|
||||
|
||||
`GODOT_HOME` 指向 Godot Mono 安装目录。成功运行时会输出 `SHRINK_GODOT_SMOKE_PASS`。
|
||||
|
||||
## 在现有 Godot C# 项目中安装
|
||||
|
||||
项目需要:
|
||||
|
||||
- Godot 4.6.3 Mono;
|
||||
- .NET 8 SDK;
|
||||
- `.csproj` 使用 `Godot.NET.Sdk/4.6.3`;
|
||||
- 项目级 `NuGet.Config` 保留既有源并加入 ShrinkSDK feed。
|
||||
|
||||
`NuGet.Config` 的最小增量如下:
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="ShrinkSDK" value="https://git.crash.work/api/packages/ShrinkSDK/nuget/index.json" />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
安装 Godot 宿主和所需模块:
|
||||
|
||||
```powershell
|
||||
dotnet add package ShrinkSDK.Godot --version 0.1.0
|
||||
dotnet add package ShrinkSDK.EventBus --version 2.1.0
|
||||
dotnet add package ShrinkSDK.Command --version 0.3.0
|
||||
dotnet add package ShrinkSDK.Network --version 0.3.0
|
||||
dotnet add package ShrinkSDK.DataSaver --version 2.3.0
|
||||
dotnet restore
|
||||
dotnet build
|
||||
```
|
||||
|
||||
只有实际使用的模块需要安装。版本以 ShrinkSDK NuGet 源中的已发布版本为准,并保留命令中的固定版本号,不要改用浮动版本。
|
||||
|
||||
## Installer 插件
|
||||
|
||||
`ShrinkSDK.Godot.Installer` 的 nupkg 内包含完整的 `addons/shrinksdk`。取得 `0.1.3` 包后,将包内:
|
||||
|
||||
```text
|
||||
contentFiles/any/any/addons/shrinksdk
|
||||
```
|
||||
|
||||
复制到 Godot 项目的:
|
||||
|
||||
```text
|
||||
addons/shrinksdk
|
||||
```
|
||||
|
||||
然后在 Godot 中打开 `Project > Project Settings > Plugins`,启用 **ShrinkSDK Installer**。左侧 ShrinkSDK 面板可以:
|
||||
|
||||
- 查看项目直接安装的 ShrinkSDK NuGet 包及版本;
|
||||
- 安装、升级或移除模块;
|
||||
- 保留项目已有 NuGet 源和无关 `PackageReference`;
|
||||
- 执行 `dotnet restore/build`;
|
||||
- 查看 CodeGen 最后一次织入结果和生成的注册数量。
|
||||
|
||||
已安装版本高于面板目录版本时会显示 `Newer` 并禁用更新按钮,不会执行降级。
|
||||
|
||||
在 Workspace 中生成 Installer 包:
|
||||
|
||||
```powershell
|
||||
dotnet build .\Assets\Modules\ShrinkInstaller\Godot~\ShrinkSDK.Godot.Installer.csproj -c Release
|
||||
dotnet pack .\Assets\Modules\ShrinkInstaller\Godot~\ShrinkSDK.Godot.Installer.csproj -c Release --no-build -o .\Godot\Artifacts
|
||||
```
|
||||
|
||||
## 宿主节点
|
||||
|
||||
主场景根节点继承 `ShrinkGodotHost`,并在 `_Ready` 中先调用 `base._Ready()`:
|
||||
|
||||
```csharp
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkSDK.Godot;
|
||||
|
||||
public partial class GameRoot : ShrinkGodotHost
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
BootAsync().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid BootAsync()
|
||||
{
|
||||
RequireWoven(GetType().Assembly);
|
||||
await StartAppAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
宿主负责日志、主线程派发、时间、`user://` 持久化路径、截图、应用生命周期和网络 dispatch queue。使用 `ShrinkNetworkService` 时,通过 `AddNetworkService(service)` 交给宿主逐帧泵送,释放前调用 `RemoveNetworkService(service)`。
|
||||
|
||||
## CodeGen 织入
|
||||
|
||||
EventBus、Command、Network 和 App 的特性注册由 `ShrinkSDK.CodeGen` 在构建时生成。项目正常引用这些模块后,`buildTransitive` 会默认设置:
|
||||
|
||||
```xml
|
||||
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
|
||||
```
|
||||
|
||||
执行:
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
```
|
||||
|
||||
日志应出现类似:
|
||||
|
||||
```text
|
||||
[ShrinkSDK.CodeGen] ... woven: instance=1, static=1, registry=2
|
||||
```
|
||||
|
||||
运行时可以在启动阶段验证程序集:
|
||||
|
||||
```csharp
|
||||
ShrinkGodotHost.RequireWoven(GetType().Assembly);
|
||||
```
|
||||
|
||||
缺少织入时应修复 NuGet 引用或构建配置,不要增加运行时反射扫描兜底。只有明确采用手工注册的普通 .NET 宿主才设置 `<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>`。
|
||||
|
||||
## 特性声明示例
|
||||
|
||||
```csharp
|
||||
using ShrinkCommand;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
|
||||
public readonly struct PlayerReadyEvent : IShrinkEvent { }
|
||||
|
||||
[ShrinkEventSubscriber]
|
||||
public sealed class PlayerReadySubscriber
|
||||
{
|
||||
[ShrinkSubscribe]
|
||||
private void OnPlayerReady(PlayerReadyEvent value) { }
|
||||
}
|
||||
|
||||
[ShrinkCommandSubscriber]
|
||||
public static class GameCommands
|
||||
{
|
||||
[ShrinkCommand("game/status")]
|
||||
public static void Status() { }
|
||||
}
|
||||
|
||||
[ShrinkNetworkMessage(41001, "game/player-ready")]
|
||||
public sealed class PlayerReadyMessage : IShrinkNetworkMessage { }
|
||||
```
|
||||
|
||||
实例订阅者使用 `EventBus.Attach(instance)` 绑定;静态订阅、Command、Network 和 App registry 由织入代码自动注册。
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource path="res://SampleRoot.cs" type="Script" id="1"]
|
||||
|
||||
[node name="SampleRoot" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,132 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using ShrinkApp;
|
||||
using ShrinkApp.Starter.Basic;
|
||||
using ShrinkCommand;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkDataSaver;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkSDK.Godot;
|
||||
using ShrinkTutorial;
|
||||
|
||||
public partial class SampleRoot : ShrinkGodotHost
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
RunSmokeAsync().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid RunSmokeAsync()
|
||||
{
|
||||
var marker = RequireWoven(GetType().Assembly);
|
||||
var goldenPath = ProjectSettings.GlobalizePath("res://../../../Assets/AgentSupportTests/Runtime/Resources/ShrinkV2Golden.bytes");
|
||||
var golden = System.IO.File.ReadAllBytes(goldenPath);
|
||||
var codec = new ShrinkJsonNetworkSerializer();
|
||||
var decoded = (CrossHostMessage)codec.Deserialize(ShrinkPacketCodec.Decode(golden).Payload, typeof(CrossHostMessage));
|
||||
using (var encoded = ShrinkPacketCodec.Encode(new ShrinkNetworkPacket { Opcode = 42, Route = "跨宿主" }, codec, decoded))
|
||||
if (decoded.Text != "跨宿主-v2" || !encoded.WrittenMemory.ToArray().SequenceEqual(golden)) throw new InvalidOperationException("Cross-host v2 frame differs.");
|
||||
using var binding = EventBus.Attach(new GodotSubscriber());
|
||||
EventBus.Post(new GodotSmokeEvent());
|
||||
var assembly = GetType().Assembly;
|
||||
var registries = assembly.GetCustomAttributes(false).Select(value => value.GetType().Name).ToHashSet();
|
||||
var savedValue = 42;
|
||||
ShrinkDataSaverRuntime.Initialize(new ShrinkDataSaverRuntimeConfig
|
||||
{
|
||||
RootPath = PersistentDataPath,
|
||||
CurrentSaveVersion = 1
|
||||
});
|
||||
ShrinkSave.RegisterModule("godot-smoke", () => savedValue, value => savedValue = value);
|
||||
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "Godot Smoke" });
|
||||
savedValue = 0;
|
||||
await ShrinkSave.LoadSlotAsync(0);
|
||||
var app = await StartAppAsync();
|
||||
var composition = ShrinkBasicComposition.CreateHost(app.Context.Services);
|
||||
var entries = ShrinkBasicComposition.CreateEntries(app.Context.Services);
|
||||
await composition.ApplyAsync(entries);
|
||||
await composition.ApplyAsync(entries);
|
||||
var activeFibers = composition.Runtime.Fibers.Count(fiber => fiber.State == ShrinkContext.ShrinkFiberState.Active);
|
||||
var tutorialStorage = new MemoryTutorialStorage();
|
||||
var tutorial = new ShrinkTutorialRunner(tutorialStorage);
|
||||
var tutorialStarted = tutorial.Start(new ShrinkTutorialData
|
||||
{
|
||||
TutorialId = "godot-smoke",
|
||||
Steps = { new ShrinkTutorialStep { StepId = "one", CompleteCondition = ShrinkTutorialCompleteCondition.AnyClick } }
|
||||
});
|
||||
var tutorialCompleted = tutorial.CompleteStep();
|
||||
|
||||
if (GodotSubscriber.Calls != 1 || GodotStaticSubscriber.Calls != 1 || savedValue != 42 ||
|
||||
!app.IsRunning || !GodotInstaller.Initialized ||
|
||||
activeFibers != 3 || !tutorialStarted || !tutorialCompleted || !tutorialStorage.Saved ||
|
||||
!registries.Contains(nameof(ShrinkCommandStaticRegistryAttribute)) ||
|
||||
!registries.Contains(nameof(ShrinkNetworkMessageRegistryAttribute)))
|
||||
{
|
||||
GD.PushError("ShrinkSDK Godot smoke failed.");
|
||||
GetTree().Quit(1);
|
||||
return;
|
||||
}
|
||||
await composition.ShutdownAsync();
|
||||
var smokePath = System.IO.Path.Combine(OS.GetUserDataDir(), "shrink_smoke_pass.txt");
|
||||
System.IO.File.WriteAllText(smokePath, $"CodeGen={marker.WeaverVersion};DataSaver={savedValue}");
|
||||
if (!System.IO.File.Exists(smokePath))
|
||||
throw new System.IO.IOException($"Failed to create smoke sentinel: {smokePath}");
|
||||
GD.Print($"SHRINK_GODOT_SMOKE_PASS CodeGen={marker.WeaverVersion} DataSaver={savedValue}");
|
||||
GetTree().Quit(0);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MemoryTutorialStorage : IShrinkTutorialStorage
|
||||
{
|
||||
private ShrinkTutorialProgress _progress = new();
|
||||
public bool Saved { get; private set; }
|
||||
public ShrinkTutorialProgress Load() => _progress;
|
||||
public void Save(ShrinkTutorialProgress progress) { _progress = progress; Saved = true; }
|
||||
public void Reset() { _progress = new ShrinkTutorialProgress(); Saved = false; }
|
||||
}
|
||||
|
||||
public readonly struct GodotSmokeEvent : IShrinkEvent { }
|
||||
|
||||
[ShrinkEventSubscriber]
|
||||
public sealed class GodotSubscriber
|
||||
{
|
||||
public static int Calls;
|
||||
[ShrinkSubscribe] private void Handle(GodotSmokeEvent value) => Calls++;
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber]
|
||||
public static class GodotStaticSubscriber
|
||||
{
|
||||
public static int Calls;
|
||||
[ShrinkSubscribe] private static void Handle(GodotSmokeEvent value) => Calls++;
|
||||
}
|
||||
|
||||
[ShrinkCommandSubscriber]
|
||||
public static class GodotCommands
|
||||
{
|
||||
[ShrinkCommand("godot/smoke")] public static void Smoke() { }
|
||||
}
|
||||
|
||||
[ShrinkNetworkMessage(42001, "godot/smoke")]
|
||||
public sealed class GodotSmokeMessage : IShrinkNetworkMessage { }
|
||||
|
||||
[ShrinkAppModuleInstaller]
|
||||
public sealed class GodotInstaller : IShrinkAppModuleInstaller
|
||||
{
|
||||
public static bool Initialized;
|
||||
public string ModuleId => "godot-smoke";
|
||||
public int Order => 0;
|
||||
public IReadOnlyList<string> DependsOn => Array.Empty<string>();
|
||||
public void RegisterServices(ShrinkAppContext context) => context.Services.Register(this);
|
||||
public UniTask InitializeAsync(ShrinkAppContext context)
|
||||
{
|
||||
Initialized = true;
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CrossHostMessage : IShrinkNetworkMessage { public string Text { get; set; } = ""; }
|
||||
@@ -0,0 +1 @@
|
||||
uid://j565ln7hmg6i
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.6.3">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
|
||||
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
|
||||
<ProjectReference Include="..\..\Packages\ShrinkSDK.Godot\ShrinkSDK.Godot.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkApp.Starter.Basic\DotNet~\ShrinkSDK.App.Starter.Basic.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkTutorial\DotNet~\ShrinkSDK.Tutorial.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkTutorial\Godot~\ShrinkSDK.Tutorial.Godot.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\ShrinkSDK.CodeGen.Task.csproj" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShrinkSDK.Godot.Sample", "ShrinkSDK.Godot.Sample.csproj", "{8ED6C49D-909C-4A5A-B80C-3537D1349377}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8ED6C49D-909C-4A5A-B80C-3537D1349377}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://TutorialDemo.cs" id="1"]
|
||||
|
||||
[node name="TutorialDemo" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,49 @@
|
||||
#nullable enable
|
||||
|
||||
using Godot;
|
||||
using ShrinkTutorial;
|
||||
using ShrinkTutorial.Godot;
|
||||
|
||||
public partial class TutorialDemo : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var target = new Button
|
||||
{
|
||||
Text = "Tutorial Target",
|
||||
Position = new Vector2(480, 260),
|
||||
CustomMinimumSize = new Vector2(220, 72)
|
||||
};
|
||||
target.AddToGroup("shrink_tutorial_primary");
|
||||
AddChild(target);
|
||||
|
||||
var overlay = new ShrinkGodotTutorialOverlay();
|
||||
AddChild(overlay);
|
||||
var runner = new ShrinkTutorialRunner(new MemoryTutorialStorage());
|
||||
runner.Start(new ShrinkTutorialData
|
||||
{
|
||||
TutorialId = "visual-smoke",
|
||||
Steps =
|
||||
{
|
||||
new ShrinkTutorialStep
|
||||
{
|
||||
StepId = "target",
|
||||
Title = "Godot Tutorial",
|
||||
Body = "Target hole, dialog and input blocker smoke test",
|
||||
TargetMode = ShrinkTutorialTargetMode.AnchorId,
|
||||
Target = "primary",
|
||||
CompleteCondition = ShrinkTutorialCompleteCondition.ClickTarget
|
||||
}
|
||||
}
|
||||
});
|
||||
overlay.Bind(runner);
|
||||
GetTree().CreateTimer(2).Timeout += QuitCleanly;
|
||||
}
|
||||
|
||||
private void QuitCleanly()
|
||||
{
|
||||
var tree = GetTree();
|
||||
tree.UnloadCurrentScene();
|
||||
tree.CreateTimer(0.1).Timeout += () => tree.Quit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bhjfn5prdv2pw
|
||||
@@ -0,0 +1,52 @@
|
||||
[preset.0]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="../../Exports/Windows/ShrinkSDKGodot.exe"
|
||||
script_export_mode=2
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
binary_format/architecture="x86_64"
|
||||
debug/export_console_wrapper=1
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux"
|
||||
runnable=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="../../Exports/Linux/ShrinkSDKGodot.x86_64"
|
||||
script_export_mode=2
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
binary_format/architecture="x86_64"
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="macOS"
|
||||
platform="macOS"
|
||||
runnable=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="../../Exports/macOS/ShrinkSDKGodot.zip"
|
||||
script_export_mode=2
|
||||
|
||||
[preset.2.options]
|
||||
|
||||
application/bundle_identifier="work.crash.shrinksdk.sample"
|
||||
binary_format/architecture="universal"
|
||||
@@ -0,0 +1,30 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="ShrinkSDK Godot Sample"
|
||||
run/main_scene="res://Main.tscn"
|
||||
config/features=PackedStringArray("4.6", "C#")
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=960
|
||||
window/size/viewport_height=540
|
||||
|
||||
[dotnet]
|
||||
|
||||
project/assembly_name="ShrinkSDK.Godot.Sample"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
textures/vram_compression/import_etc2_astc=true
|
||||
@@ -0,0 +1,45 @@
|
||||
<Solution>
|
||||
<Folder Name="/Packages/">
|
||||
<Project Path="../Assets/Modules/ShrinkRuntime.Abstractions/DotNet~/ShrinkSDK.Runtime.Abstractions.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkContext.Core/DotNet~/ShrinkSDK.Context.Core.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkEventBus/DotNet~/ShrinkSDK.EventBus.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkCommand/DotNet~/ShrinkSDK.Command.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkNetwork/DotNet~/ShrinkSDK.Network.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkCommand.Integration.EventBus/DotNet~/ShrinkSDK.Command.Integration.EventBus.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkCommand.Integration.Network/DotNet~/ShrinkSDK.Command.Integration.Network.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkCommand.Integration.App/DotNet~/ShrinkSDK.Command.Integration.App.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkDataSaver.Integration.EventBus/DotNet~/ShrinkSDK.DataSaver.Integration.EventBus.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkDataSaver.Integration.App/DotNet~/ShrinkSDK.DataSaver.Integration.App.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkNetwork.Integration.EventBus/DotNet~/ShrinkSDK.Network.Integration.EventBus.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkNetwork.Integration.App/DotNet~/ShrinkSDK.Network.Integration.App.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkDataSaver/DotNet~/ShrinkSDK.DataSaver.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkApp.Core/DotNet~/ShrinkSDK.App.Core.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkContext.AppAdapter/DotNet~/ShrinkSDK.Context.AppAdapter.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkApp.Starter.Basic/DotNet~/ShrinkSDK.App.Starter.Basic.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkModFramework/DotNet~/ShrinkSDK.ModFramework.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkModFramework/Godot~/ShrinkSDK.ModFramework.Godot.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkTutorial/DotNet~/ShrinkSDK.Tutorial.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkTutorial/Godot~/ShrinkSDK.Tutorial.Godot.csproj" />
|
||||
<Project Path="Packages/ShrinkSDK.Godot/ShrinkSDK.Godot.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/CodeGen/">
|
||||
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Core/ShrinkSDK.CodeGen.Core.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj" />
|
||||
<Project Path="../Assets/Modules/ShrinkShared.CodeGen/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Tests/">
|
||||
<Project Path="Tests/CodeGenFixture/CodeGenFixture.csproj" />
|
||||
<Project Path="Tests/NuGetConsumer/NuGetConsumer.csproj" />
|
||||
<Project Path="Tests/CodeGenValidation/CodeGenValidation.csproj" />
|
||||
<Project Path="Tests/UnityAdapterCompile/UnityAdapterCompile.csproj" />
|
||||
<Project Path="Tests/ModFixture/ModFixture.csproj" />
|
||||
<Project Path="Tests/ModHostFixture/ModHostFixture.csproj" />
|
||||
<Project Path="Tests/InstallerFixture/InstallerFixture.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/">
|
||||
<Project Path="Samples/ShrinkSDK.Godot.Sample/ShrinkSDK.Godot.Sample.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Installer/">
|
||||
<Project Path="../Assets/Modules/ShrinkInstaller/Godot~/ShrinkSDK.Godot.Installer.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>ShrinkSDK.CodeGenFixture</AssemblyName>
|
||||
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
|
||||
<SignAssembly Condition="'$(FixtureSignAssembly)' == 'true'">true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile Condition="'$(FixtureSignAssembly)' == 'true'">$(FixtureStrongNameKeyFile)</AssemblyOriginatorKeyFile>
|
||||
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork.Integration.EventBus\DotNet~\ShrinkSDK.Network.Integration.EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\ShrinkSDK.CodeGen.Task.csproj" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ShrinkCommand;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkNetwork;
|
||||
using ShrinkNetwork.Integration;
|
||||
using ShrinkSDK.Runtime;
|
||||
|
||||
var assembly = typeof(Program).Assembly;
|
||||
var marker = assembly.GetCustomAttributes(typeof(ShrinkCodeGenWovenAttribute), false)
|
||||
.Cast<ShrinkCodeGenWovenAttribute>()
|
||||
.SingleOrDefault() ?? throw new InvalidOperationException("CodeGen marker is missing.");
|
||||
if (marker.WeaverVersion != "0.1.0" || !Guid.TryParse(marker.InputMvid, out _))
|
||||
throw new InvalidOperationException("CodeGen marker is invalid.");
|
||||
|
||||
if (!typeof(InstanceSubscriber).GetInterfaces().Contains(typeof(IShrinkGeneratedSubscriber)))
|
||||
throw new InvalidOperationException("Generated subscriber interface is missing.");
|
||||
|
||||
using var binding = EventBus.Attach(new InstanceSubscriber());
|
||||
EventBus.Post(new FixtureEvent());
|
||||
if (InstanceSubscriber.Calls != 1 || StaticSubscriber.Calls != 1)
|
||||
throw new InvalidOperationException($"Generated EventBus bindings failed: instance={InstanceSubscriber.Calls}, static={StaticSubscriber.Calls}.");
|
||||
|
||||
if (assembly.GetCustomAttributes(typeof(ShrinkCommandStaticRegistryAttribute), false).Length != 1)
|
||||
throw new InvalidOperationException("Command registry metadata is missing.");
|
||||
if (assembly.GetCustomAttributes(typeof(ShrinkNetworkMessageRegistryAttribute), false).Length != 1)
|
||||
throw new InvalidOperationException("Network message registry metadata is missing.");
|
||||
if (assembly.GetCustomAttributes(typeof(ShrinkNetworkStaticSubscriberRegistryAttribute), false).Length != 1)
|
||||
throw new InvalidOperationException("Network subscriber registry metadata is missing.");
|
||||
var bridgeRegistrations = (System.Collections.IEnumerable)(typeof(ShrinkNetworkEventRegistry)
|
||||
.GetMethod("Snapshot", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
|
||||
.Invoke(null, null) ?? throw new InvalidOperationException("Network EventBus registry snapshot failed."));
|
||||
if (!bridgeRegistrations.Cast<object>().Any(item => item.GetType().GetProperty("EventType")?.GetValue(item) as Type == typeof(FixtureNetworkEvent)))
|
||||
throw new InvalidOperationException("Generated Network/EventBus binding is missing.");
|
||||
|
||||
Console.WriteLine($"PASS CodeGen {marker.WeaverVersion}: EventBus + Command + Network + Network/EventBus registries");
|
||||
|
||||
public readonly struct FixtureEvent : IShrinkEvent { }
|
||||
|
||||
[ShrinkEventSubscriber]
|
||||
public sealed class InstanceSubscriber
|
||||
{
|
||||
public static int Calls;
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private void OnEvent(FixtureEvent value) => Calls++;
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber]
|
||||
public static class StaticSubscriber
|
||||
{
|
||||
public static int Calls;
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private static void OnEvent(FixtureEvent value) => Calls++;
|
||||
}
|
||||
|
||||
[ShrinkCommandSubscriber]
|
||||
public static class FixtureCommands
|
||||
{
|
||||
[ShrinkCommand("fixture/ping")]
|
||||
public static void Ping() { }
|
||||
}
|
||||
|
||||
[ShrinkNetworkMessage(41001, "fixture/ping")]
|
||||
public sealed class FixtureMessage : IShrinkNetworkMessage { }
|
||||
|
||||
[ShrinkNetworkEvent]
|
||||
[ShrinkNetworkMessage(41002, "fixture/event")]
|
||||
public sealed class FixtureNetworkEvent : IShrinkEvent, IShrinkNetworkMessage { }
|
||||
|
||||
[ShrinkNetworkSubscriber]
|
||||
public static class FixtureNetworkHandlers
|
||||
{
|
||||
[ShrinkNetworkSubscribe]
|
||||
public static void Handle(FixtureMessage message) { }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>ShrinkSDK.CodeGenValidation</AssemblyName>
|
||||
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Core\ShrinkSDK.CodeGen.Core.csproj" />
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,163 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Mono.Cecil;
|
||||
using ShrinkSDK.CodeGen;
|
||||
|
||||
if (args.Length == 2 && args[0] == "--marker")
|
||||
{
|
||||
using var exported = AssemblyDefinition.ReadAssembly(args[1]);
|
||||
var marker = exported.CustomAttributes.SingleOrDefault(attribute =>
|
||||
attribute.AttributeType.FullName == "ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute");
|
||||
Require(marker != null, "exported assembly has no ShrinkSDK CodeGen marker");
|
||||
Require(exported.MainModule.Types.Any(type => type.FullName ==
|
||||
"ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings"),
|
||||
"exported assembly has no static EventBus bootstrap");
|
||||
Console.WriteLine($"PASS exported assembly woven by {marker!.ConstructorArguments[0].Value}");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((args.Length == 3 || args.Length == 4) && args[0] == "--resign")
|
||||
{
|
||||
var signedAssembly = Path.GetFullPath(args[1]);
|
||||
var keepOutput = args.Length == 4;
|
||||
var output = keepOutput
|
||||
? Path.GetFullPath(args[3])
|
||||
: Path.Combine(Path.GetTempPath(), "ShrinkSDK.CodeGen.Resigned." + Guid.NewGuid().ToString("N") + ".dll");
|
||||
var outputPdb = Path.ChangeExtension(output, ".pdb");
|
||||
try
|
||||
{
|
||||
var result = ShrinkAssemblyWeaver.Weave(signedAssembly, Path.ChangeExtension(signedAssembly, ".pdb"),
|
||||
BuildReferences(Path.GetDirectoryName(signedAssembly)!), output, outputPdb,
|
||||
ShrinkCodeGenPlatform.EngineNeutral, args[2]);
|
||||
Require(result.Succeeded && result.Changed,
|
||||
"signed assembly re-weave failed: " + string.Join(" | ", result.Diagnostics.Select(item => item.Message)));
|
||||
using var resigned = AssemblyDefinition.ReadAssembly(output);
|
||||
Require(resigned.Name.HasPublicKey, "re-signed output has no public key");
|
||||
Console.WriteLine("PASS signed assembly woven and re-signed with explicit key");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!keepOutput)
|
||||
{
|
||||
if (File.Exists(output)) File.Delete(output);
|
||||
if (File.Exists(outputPdb)) File.Delete(outputPdb);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length != 1 || !File.Exists(args[0]))
|
||||
throw new ArgumentException("Pass the unwoven CodeGenFixture assembly path.");
|
||||
|
||||
var sourceAssembly = Path.GetFullPath(args[0]);
|
||||
var sourceDirectory = Path.GetDirectoryName(sourceAssembly)!;
|
||||
var sourcePdb = Path.ChangeExtension(sourceAssembly, ".pdb");
|
||||
var references = BuildReferences(sourceDirectory);
|
||||
var root = Path.Combine(Path.GetTempPath(), "ShrinkSDK.CodeGen.Validation", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
var woven = Path.Combine(root, "fixture.woven.dll");
|
||||
var wovenPdb = Path.Combine(root, "fixture.woven.pdb");
|
||||
var first = ShrinkAssemblyWeaver.Weave(sourceAssembly, sourcePdb, references, woven, wovenPdb);
|
||||
Require(first.Succeeded && first.Changed,
|
||||
"single weave did not change the assembly: " + string.Join(" | ", first.Diagnostics.Select(item => item.Message)));
|
||||
Require(first.InstanceSubscribers == 1 && first.StaticSubscribers == 1 && first.RegistryEntries == 5,
|
||||
$"unexpected generated counts: {first.InstanceSubscribers}/{first.StaticSubscribers}/{first.RegistryEntries}");
|
||||
VerifyStructure(woven);
|
||||
|
||||
var second = ShrinkAssemblyWeaver.Weave(woven, wovenPdb, references.Append(woven),
|
||||
Path.Combine(root, "twice.dll"), Path.Combine(root, "twice.pdb"));
|
||||
Require(second.Succeeded && !second.Changed, "repeated weave was not idempotent");
|
||||
|
||||
var corruptPdb = Path.Combine(root, "corrupt.pdb");
|
||||
File.WriteAllText(corruptPdb, "not a portable pdb");
|
||||
var corrupt = ShrinkAssemblyWeaver.Weave(sourceAssembly, corruptPdb, references,
|
||||
Path.Combine(root, "corrupt.dll"), Path.Combine(root, "corrupt.out.pdb"));
|
||||
Require(!corrupt.Succeeded && !File.Exists(Path.Combine(root, "corrupt.dll")),
|
||||
"damaged PDB was not rejected atomically");
|
||||
|
||||
var missing = ShrinkAssemblyWeaver.Weave(sourceAssembly, null, Array.Empty<string>(),
|
||||
Path.Combine(root, "missing.dll"), null);
|
||||
Require(!missing.Succeeded, "missing dependencies were not rejected");
|
||||
|
||||
var signedInput = Path.Combine(root, "signed.dll");
|
||||
using (var assembly = AssemblyDefinition.ReadAssembly(sourceAssembly))
|
||||
{
|
||||
assembly.Name.PublicKey = Enumerable.Range(0, 160).Select(value => (byte)(value + 1)).ToArray();
|
||||
assembly.Name.Attributes |= AssemblyAttributes.PublicKey;
|
||||
assembly.Write(signedInput);
|
||||
}
|
||||
var signed = ShrinkAssemblyWeaver.Weave(signedInput, null, references,
|
||||
Path.Combine(root, "signed.out.dll"), null);
|
||||
Require(!signed.Succeeded && signed.Diagnostics.Any(item => item.Message.Contains("Signed assemblies")),
|
||||
"signed assembly was not rejected");
|
||||
|
||||
var parallel = Enumerable.Range(0, 4).Select(index => Task.Run(() =>
|
||||
{
|
||||
var output = Path.Combine(root, $"parallel-{index}.dll");
|
||||
var outputPdb = Path.Combine(root, $"parallel-{index}.pdb");
|
||||
return ShrinkAssemblyWeaver.Weave(sourceAssembly, sourcePdb, references, output, outputPdb);
|
||||
})).ToArray();
|
||||
await Task.WhenAll(parallel);
|
||||
Require(parallel.All(task => task.Result.Succeeded && task.Result.Changed),
|
||||
"parallel weaving of independent assemblies failed");
|
||||
|
||||
Console.WriteLine("PASS Cecil structure + single/repeat/parallel weave + corrupt PDB/signed/missing dependency guards");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, true);
|
||||
}
|
||||
|
||||
static void VerifyStructure(string path)
|
||||
{
|
||||
using var assembly = AssemblyDefinition.ReadAssembly(path);
|
||||
var module = assembly.MainModule;
|
||||
var instance = module.Types.Single(type => type.Name == "InstanceSubscriber");
|
||||
Require(instance.Interfaces.Any(item => item.InterfaceType.FullName == "ShrinkEventBus.IShrinkGeneratedSubscriber"),
|
||||
"generated subscriber interface is missing");
|
||||
Require(instance.Methods.Any(method => method.Name == "ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated"),
|
||||
"AttachGenerated is missing");
|
||||
Require(module.Types.Any(type => type.FullName == "ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings"),
|
||||
"static binding bootstrap is missing");
|
||||
Require(module.Types.Any(type => type.FullName ==
|
||||
"ShrinkNetwork.Integration.Generated.ShrinkGeneratedNetworkEventBindings"),
|
||||
"Network/EventBus generated bootstrap is missing");
|
||||
var moduleInitializer = module.Types.Single(type => type.Name == "<Module>").Methods.Single(method => method.Name == ".cctor");
|
||||
Require(moduleInitializer.Body.Instructions.Any(instruction => instruction.Operand is MethodReference method && method.Name == "Register"),
|
||||
"module initializer does not call static registration");
|
||||
var attributes = assembly.CustomAttributes.Select(attribute => attribute.AttributeType.FullName).ToArray();
|
||||
foreach (var expected in new[]
|
||||
{
|
||||
"ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute",
|
||||
"ShrinkCommand.ShrinkCommandStaticRegistryAttribute",
|
||||
"ShrinkNetwork.ShrinkNetworkMessageRegistryAttribute",
|
||||
"ShrinkNetwork.ShrinkNetworkStaticSubscriberRegistryAttribute"
|
||||
})
|
||||
Require(attributes.Contains(expected), $"assembly registry is missing: {expected}");
|
||||
}
|
||||
|
||||
static void Require(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
static string[] BuildReferences(string sourceDirectory)
|
||||
{
|
||||
var referencePackRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".nuget", "packages", "microsoft.netcore.app.ref");
|
||||
var referencePack = Directory.GetDirectories(referencePackRoot)
|
||||
.Select(path => Path.Combine(path, "ref", "net8.0"))
|
||||
.Where(Directory.Exists)
|
||||
.OrderByDescending(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.First();
|
||||
return Directory.GetFiles(sourceDirectory, "*.dll")
|
||||
.Concat(Directory.GetFiles(referencePack, "*.dll"))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>ShrinkSDK.InstallerFixture</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkInstaller\Godot~\ShrinkSDK.Godot.Installer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,65 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using ShrinkSDK.Godot.Editor;
|
||||
using ShrinkSDK.Installer;
|
||||
|
||||
var root = Path.Combine(Path.GetTempPath(), "ShrinkSDK.InstallerFixture", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
var project = Path.Combine(root, "Fixture.csproj");
|
||||
File.WriteAllText(project, """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><TargetFramework>net8.0</TargetFramework><FixtureValue>keep</FixtureValue></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Existing.Package" Version="1.2.3" />
|
||||
<PackageReference Include="Child.Version.Package"><Version>3.4.5</Version></PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""");
|
||||
var config = Path.Combine(root, "NuGet.Config");
|
||||
File.WriteAllText(config, """
|
||||
<configuration><packageSources><add key="Existing" value="https://example.invalid/v3/index.json" /></packageSources></configuration>
|
||||
""");
|
||||
|
||||
ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", "2.1.0");
|
||||
ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", "2.1.0");
|
||||
var installed = XDocument.Load(project);
|
||||
Require(installed.Descendants("PackageReference").Count(element =>
|
||||
(string?)element.Attribute("Include") == "ShrinkSDK.EventBus") == 1, "package reference was duplicated");
|
||||
Require(installed.ToString().Contains("Existing.Package") && installed.ToString().Contains("FixtureValue"),
|
||||
"unrelated project content was changed");
|
||||
var installedVersions = ShrinkProjectPackageEditor.ReadPackageReferences(project);
|
||||
Require(installedVersions["ShrinkSDK.EventBus"] == "2.1.0", "installed version was not read correctly");
|
||||
Require(installedVersions["Child.Version.Package"] == "3.4.5", "child Version element was not read correctly");
|
||||
Require(ShrinkPackageVersion.Classify("2.2.0", "2.1.0") == ShrinkPackageVersionRelation.Newer,
|
||||
"newer installed version was classified as outdated");
|
||||
Require(ShrinkPackageVersion.Classify("2.1.0-preview.10", "2.1.0-preview.2") ==
|
||||
ShrinkPackageVersionRelation.Newer, "prerelease versions were not ordered semantically");
|
||||
|
||||
ShrinkProjectPackageEditor.SetPackageReference(project, "ShrinkSDK.EventBus", null);
|
||||
Require(!XDocument.Load(project).Descendants("PackageReference").Any(element =>
|
||||
(string?)element.Attribute("Include") == "ShrinkSDK.EventBus"), "package reference was not removed");
|
||||
|
||||
ShrinkProjectPackageEditor.EnsureShrinkFeed(config);
|
||||
ShrinkProjectPackageEditor.EnsureShrinkFeed(config);
|
||||
var sources = XDocument.Load(config).Descendants("add").ToArray();
|
||||
Require(sources.Count(element => (string?)element.Attribute("key") == "ShrinkSDK") == 1,
|
||||
"ShrinkSDK feed was duplicated");
|
||||
Require(sources.Any(element => (string?)element.Attribute("key") == "Existing"),
|
||||
"existing NuGet source was removed");
|
||||
Console.WriteLine("PASS Godot Installer atomic package/feed install-update-remove merge");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, true);
|
||||
}
|
||||
|
||||
static void Require(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ShrinkNetwork;
|
||||
|
||||
[ShrinkNetworkMessage(49999, "duplicate")]
|
||||
public sealed class DuplicateMessageA : IShrinkNetworkMessage { }
|
||||
|
||||
[ShrinkNetworkMessage(49999, "duplicate")]
|
||||
public sealed class DuplicateMessageB : IShrinkNetworkMessage { }
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>ShrinkSDK.InvalidCodeGenFixture</AssemblyName>
|
||||
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
|
||||
<RunAnalyzers>false</RunAnalyzers>
|
||||
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkNetwork\DotNet~\ShrinkSDK.Network.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\ShrinkSDK.CodeGen.Task.csproj" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
using ShrinkCommand;
|
||||
using ShrinkEventBus;
|
||||
using ShrinkModFramework;
|
||||
|
||||
[ShrinkMod("fixture.mod", "Fixture Mod", "1.0.0")]
|
||||
public sealed class FixtureMod : ShrinkModBase, IShrinkModUnload
|
||||
{
|
||||
public bool Ready { get; private set; }
|
||||
public int UnloadCalls { get; private set; }
|
||||
public override void OnReady(ShrinkModContext context) => Ready = true;
|
||||
public void OnUnload(ShrinkModContext context) => UnloadCalls++;
|
||||
}
|
||||
|
||||
public readonly struct ModFixtureEvent : IShrinkEvent { }
|
||||
|
||||
[ShrinkEventSubscriber]
|
||||
public static class ModFixtureEvents
|
||||
{
|
||||
public static int Calls { get; private set; }
|
||||
[ShrinkSubscribe] private static void Handle(ModFixtureEvent value) => Calls++;
|
||||
}
|
||||
|
||||
[ShrinkCommandSubscriber]
|
||||
public static class ModFixtureCommands
|
||||
{
|
||||
[ShrinkCommand("mod-fixture/ping")] public static void Ping() { }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<AssemblyName>ShrinkSDK.ModFixture</AssemblyName>
|
||||
<ShrinkCodeGenEnabled>true</ShrinkCodeGenEnabled>
|
||||
<_ShrinkCodeGenTaskAssembly>$(MSBuildThisFileDirectory)..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\bin\$(Configuration)\net8.0\ShrinkSDK.CodeGen.Task.dll</_ShrinkCodeGenTaskAssembly>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkRuntime.Abstractions\DotNet~\ShrinkSDK.Runtime.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkModFramework\DotNet~\ShrinkSDK.ModFramework.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\..\Assets\Modules\ShrinkShared.CodeGen\DotNet~\ShrinkSDK.CodeGen.Task\buildTransitive\ShrinkSDK.CodeGen.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>ShrinkSDK.ModHostFixture</AssemblyName>
|
||||
<ShrinkCodeGenEnabled>false</ShrinkCodeGenEnabled>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkModFramework\Godot~\ShrinkSDK.ModFramework.Godot.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkEventBus\DotNet~\ShrinkSDK.EventBus.csproj" />
|
||||
<ProjectReference Include="..\..\..\Assets\Modules\ShrinkCommand\DotNet~\ShrinkSDK.Command.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user