mirror of
https://github.com/stride3d/stride.git
synced 2026-10-12 02:23:23 +00:00
Merge branch 'master' into feature/xplat/rename-projectcs
This commit is contained in:
@@ -34,7 +34,9 @@ concurrency:
|
||||
jobs:
|
||||
Windows-MSBuild:
|
||||
name: Windows Full (VS MSBuild, ${{ github.event.inputs.build-type || 'Debug' }})
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
|
||||
if: >-
|
||||
(github.event_name != 'schedule' || vars.STRIDE_ENABLE_SCHEDULED_CI == 'true')
|
||||
&& (github.event_name != 'pull_request' || github.event.pull_request.draft != true)
|
||||
runs-on: windows-2025-vs2026
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
|
||||
@@ -72,6 +72,37 @@ jobs:
|
||||
return bool(files)
|
||||
print('comment_only=' + str(comment_only()).lower())
|
||||
EOF
|
||||
# Users get the samples from the published content template packs (StrideSamplesVersion.props), not from
|
||||
# the repository, so a sample change only reaches them through a samples release (release-samples.yml).
|
||||
# Editing samples is normal work, so this only warns, as a reminder that one is pending. The engine-versioned
|
||||
# template sources and prose don't count.
|
||||
- name: Warn on sample changes (samples release needed)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
python3 - <<'EOF'
|
||||
import json, re
|
||||
files = [f for page in json.load(open('/tmp/pr-files.json')) for f in page]
|
||||
def is_content(name):
|
||||
return (name.startswith('samples/')
|
||||
and not name.startswith(('samples/NewGame/', 'samples/Library/'))
|
||||
and not name.lower().endswith('.md'))
|
||||
changed = [f for f in files if is_content(f['filename']) or is_content(f.get('previous_filename') or '')]
|
||||
if changed:
|
||||
names = [f['filename'] for f in changed]
|
||||
shown = ', '.join(names[:5]) + (f' and {len(names) - 5} more' if len(names) > 5 else '')
|
||||
# The PR diff shows an annotation only on a file of the new version, on a line inside the diff: the
|
||||
# first changed sample file that still exists and has a text diff, at the start of its first changed
|
||||
# block. Deleted and binary-only changes leave the warning on the run summary only.
|
||||
where = ''
|
||||
for f in changed:
|
||||
hunk = re.search(r'^@@ -\d+(?:,\d+)? \+(\d+)', f.get('patch') or '', re.M)
|
||||
if f['status'] != 'removed' and hunk and int(hunk.group(1)) > 0:
|
||||
where = f"file={f['filename']},line={hunk.group(1)},"
|
||||
break
|
||||
print('::warning ' + where + 'title=Samples changed::This PR changes the samples (' + shown + '). '
|
||||
'Users only get them with the next samples release: after merging, run release-samples.yml '
|
||||
'(before the next engine release if the engine needs them; see docs/build/versioning.md).')
|
||||
EOF
|
||||
- uses: dorny/paths-filter@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
id: filter
|
||||
|
||||
@@ -81,6 +81,8 @@ jobs:
|
||||
build-type: Debug # Release has a bug and cannot build
|
||||
|
||||
### Runtimes ###
|
||||
# The `ios` filter is narrower than `runtime` on purpose: iOS runners are costly, macOS-Tests-Game
|
||||
# covers the shared engine surface per-PR, and master pushes run this per-commit anyway.
|
||||
iOS-Runtime:
|
||||
needs: changes
|
||||
if: (github.event.pull_request.draft != true || contains(github.event.pull_request.labels.*.name, 'ci-run-on-draft')) && needs.changes.outputs.ios == 'true' && (github.event_name != 'workflow_dispatch' || inputs.ios)
|
||||
|
||||
@@ -125,24 +125,62 @@ jobs:
|
||||
}
|
||||
echo '```' >> $env:GITHUB_STEP_SUMMARY
|
||||
|
||||
# The content template packs (Samples, Starters, AssetPacks) are not part of an engine release: the engine
|
||||
# names their exact version (sources/templates/StrideSamplesVersion.props), which release-samples.yml only commits
|
||||
# once it is published. Refuse an engine whose content is not on the feed (every New Project would break) or
|
||||
# that is stable while naming prerelease content. Samples changed since the published content (its
|
||||
# samples/<version> tag) only warn: the engine ships with the published content, which is right for work not
|
||||
# released yet, and the release manager judges whether a change is one the new engine needs.
|
||||
- name: Check the content version
|
||||
shell: bash
|
||||
run: |
|
||||
SHA="${{ steps.src.outputs.sha }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
FILE=sources/templates/StrideSamplesVersion.props
|
||||
SV=$(git show "$SHA:$FILE" | sed -n 's|.*<_StrideCommittedSamplesVersion>\([^<]*\)</.*|\1|p')
|
||||
if [ -z "$SV" ]; then
|
||||
echo "::error::Could not read the content version from $FILE at $SHA"
|
||||
exit 1
|
||||
fi
|
||||
echo "::notice::Content version named by this engine: $SV"
|
||||
case "$VERSION" in
|
||||
*-*) ;;
|
||||
*) case "$SV" in *-*) echo "::error::Stable engine $VERSION names prerelease content $SV. Publish the samples with a stable content number first (release-samples.yml)."; exit 1;; esac;;
|
||||
esac
|
||||
for id in stride.templates.samples stride.templates.games.starters stride.templates.assetpacks; do
|
||||
if ! curl -sf "https://api.nuget.org/v3-flatcontainer/$id/index.json" | grep -q "\"$SV\""; then
|
||||
echo "::error::$id $SV is not on nuget.org. Publish the samples first (release-samples.yml), then release the engine."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# The commit the published content was packed from (content published before release-samples.yml has
|
||||
# no such tag).
|
||||
if ! git rev-parse -q --verify "refs/tags/samples/$SV" >/dev/null; then
|
||||
echo "::warning::No samples/$SV tag; skipped the check for sample changes since the published content."
|
||||
exit 0
|
||||
fi
|
||||
CHANGED=$(git diff --name-only "samples/$SV" "$SHA" -- samples ':(exclude)samples/NewGame' ':(exclude)samples/Library' ':(exclude)*.md')
|
||||
if [ -n "$CHANGED" ]; then
|
||||
COUNT=$(printf '%s\n' "$CHANGED" | wc -l)
|
||||
echo "::warning title=Samples changed::$COUNT sample file(s) changed since the published content $SV (tag samples/$SV). This engine ships with the published $SV content. If the engine needs those changes, publish a samples release first (see the run summary)."
|
||||
{
|
||||
echo "## Sample changes not in the published content ($SV)"
|
||||
echo ""
|
||||
echo "This engine ships with the published $SV content. Fine for work not released yet; if the new engine needs these changes, publish a samples release with release-samples.yml (it updates SamplesVersion), then release again."
|
||||
echo ""
|
||||
echo '```'
|
||||
printf '%s\n' "$CHANGED" | sed -n '1,50p'
|
||||
[ "$COUNT" -gt 50 ] && echo "... and $((COUNT - 50)) more"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Push NuGet packages
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Content-versioned template packages (Samples, Starters) follow StrideSamplesVersion and
|
||||
# only change when the samples are bumped (see plans/samples-version-bump.md). Skip the
|
||||
# upload when that version is already on nuget.org - re-publishing the same version is a
|
||||
# no-op and these packages are large, so this avoids a wasted upload every release.
|
||||
function Test-NuGetPublished([string]$id, [string]$version) {
|
||||
$url = "https://api.nuget.org/v3-flatcontainer/$($id.ToLowerInvariant())/index.json"
|
||||
try { return ((Invoke-RestMethod -Uri $url -TimeoutSec 60 -ErrorAction Stop).versions -contains $version) }
|
||||
catch { return $false } # 404 (never published) or transient: fall through to push (--skip-duplicate stays safe)
|
||||
}
|
||||
|
||||
$packages = Get-ChildItem -Path bin/packages -Filter "*.nupkg"
|
||||
$contentVersionedIds = @('Stride.Templates.Samples', 'Stride.Templates.Games.Starters', 'Stride.Templates.AssetPacks')
|
||||
$contentVersioned = $packages | Where-Object { $_.Name -match 'Stride\.Templates\.(Samples|Games\.Starters|AssetPacks)\.' }
|
||||
$gameStudio = $packages | Where-Object { $_.Name -match 'GameStudio' }
|
||||
$main = $packages | Where-Object { $_.Name -notmatch 'GameStudio' -and $_.Name -notmatch 'Stride\.Templates\.(Samples|Games\.Starters|AssetPacks)\.' }
|
||||
$main = $packages | Where-Object { $_.Name -notmatch 'GameStudio' }
|
||||
|
||||
echo "::group::Pushing main packages ($($main.Count))"
|
||||
foreach ($pkg in $main) {
|
||||
@@ -151,20 +189,6 @@ jobs:
|
||||
}
|
||||
echo "::endgroup::"
|
||||
|
||||
if ($contentVersioned) {
|
||||
echo "::group::Pushing content-versioned template packages (Samples, Starters, AssetPacks)"
|
||||
foreach ($pkg in $contentVersioned) {
|
||||
$id = $contentVersionedIds | Where-Object { $pkg.Name.StartsWith($_ + '.') } | Select-Object -First 1
|
||||
if ($id -and (Test-NuGetPublished $id $pkg.BaseName.Substring($id.Length + 1))) {
|
||||
echo "Skipping $($pkg.Name) (already published; samples unchanged since last release)."
|
||||
continue
|
||||
}
|
||||
echo "Pushing $($pkg.Name)..."
|
||||
dotnet nuget push $pkg.FullName --api-key $env:STRIDE_NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --timeout 1800 --skip-duplicate
|
||||
}
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
if ($gameStudio) {
|
||||
echo "::group::Pushing GameStudio (last, so dependencies are already available)"
|
||||
foreach ($pkg in $gameStudio) {
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
name: Release Samples
|
||||
|
||||
# Builds, signs, and (optionally) deploys the content template packs (Stride.Templates.Samples,
|
||||
# Stride.Templates.Games.Starters, Stride.Templates.AssetPacks) to NuGet.org.
|
||||
#
|
||||
# Versioned independently of the engine: StrideSamplesVersion in sources/templates/StrideSamplesVersion.props is the
|
||||
# published content version the engine uses. This workflow is the only thing that changes it: it packs the samples
|
||||
# of the branch under the next content version, publishes them, and only then commits that version to the branch,
|
||||
# so the branch never names content that is not on the feed. The packs are stamped for this branch's committed engine
|
||||
# version (MajorMinor.Patch + NuGetVersionSuffix), the engine release they precede.
|
||||
# A deploy pushes to NuGet.org, tags samples/<version> at the built commit, creates a GitHub Release and commits
|
||||
# SamplesVersion to the branch. A run without deploy publishes and commits nothing.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Content version to publish (e.g. 4.4.1, or 4.4.1-beta1 for content not ready yet). Empty = the committed SamplesVersion's next stable number, or <engine line>.0 for older-line content.
|
||||
default: ''
|
||||
type: string
|
||||
sign:
|
||||
description: Code sign the packages
|
||||
default: true
|
||||
type: boolean
|
||||
deploy:
|
||||
description: Deploy to NuGet.org, create a GitHub Release and commit SamplesVersion to the branch
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: release-samples-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
Validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check deploy requires signing
|
||||
if: ${{ inputs.deploy && !inputs.sign }}
|
||||
run: |
|
||||
echo "::error::Cannot deploy unsigned packages. Enable 'sign' to deploy."
|
||||
exit 1
|
||||
|
||||
- name: Check release permissions
|
||||
if: ${{ inputs.sign || inputs.deploy }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
ORG=$(echo "${{ github.repository }}" | cut -d/ -f1)
|
||||
TEAM="stride-release-managers"
|
||||
USER="${{ github.actor }}"
|
||||
if ! gh api "orgs/$ORG/teams/$TEAM/memberships/$USER" --silent 2>/dev/null; then
|
||||
echo "::error::User $USER is not a member of $ORG/$TEAM. Sign/deploy requires stride-release-managers team membership."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Package:
|
||||
name: Package
|
||||
needs: Validate
|
||||
runs-on: windows-2025-vs2026
|
||||
environment: production # signing secrets live here
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
engine: ${{ steps.check.outputs.engine }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
lfs: true
|
||||
fetch-depth: 0 # full history + tags for the re-publish guard
|
||||
fetch-tags: true
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
# The content version: the input, or the committed one's next number (4.4.0 -> 4.4.1, 4.4.0-beta7 -> 4.4.0), or
|
||||
# <engine line>.0 when the committed content is from an older line. Its major.minor is this branch's engine line
|
||||
# (a line's content is released from that line's branch only), its patch part is a free counter.
|
||||
- name: Choose the content version
|
||||
id: version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$file = Get-Content sources/shared/SharedAssemblyInfo.cs -Raw
|
||||
$committed = [regex]::Match((Get-Content sources/templates/StrideSamplesVersion.props -Raw), '<_StrideCommittedSamplesVersion>([^<]*)</').Groups[1].Value
|
||||
$engineLine = [regex]::Match($file, 'MajorMinor = "([^"]*)";').Groups[1].Value
|
||||
$version = "${{ inputs.version }}".Trim()
|
||||
$parts = [regex]::Match($committed, '^(\d+\.\d+)\.(\d+)(-.*)?$')
|
||||
if (-not $parts.Success) {
|
||||
echo "::error::Could not read the content version ('$committed') from sources/templates/StrideSamplesVersion.props"
|
||||
exit 1
|
||||
}
|
||||
$committedLine, $committedPatch = $parts.Groups[1].Value, [int]$parts.Groups[2].Value
|
||||
if ($version -eq '') {
|
||||
if ($committedLine -ne $engineLine) { $version = "$engineLine.0" }
|
||||
elseif ($parts.Groups[3].Success) { $version = "$committedLine.$committedPatch" }
|
||||
else { $version = "$committedLine.$($committedPatch + 1)" }
|
||||
}
|
||||
# Usually stable, so an engine going from beta to stable keeps the same content (New Project rewrites the
|
||||
# Stride.* references to the running engine). A prerelease (typed, never the default) is for content not
|
||||
# ready yet: release-deploy.yml refuses a stable engine that names it.
|
||||
if ($version -notmatch '^\d+\.\d+\.\d+(-(alpha|beta|rc|preview)\d+)?$') {
|
||||
echo "::error::Content version '$version' is not an allowed release version. Use X.Y.Z or X.Y.Z-(alpha|beta|rc|preview)<N>."
|
||||
exit 1
|
||||
}
|
||||
if (($version -replace '^(\d+\.\d+).*', '$1') -ne $engineLine) {
|
||||
echo "::error::Content version $version is not on this branch's engine line $engineLine. Release $($version -replace '^(\d+\.\d+).*', '$1').x content from the branch of that line."
|
||||
exit 1
|
||||
}
|
||||
if ([System.Management.Automation.SemanticVersion]$version -le [System.Management.Automation.SemanticVersion]$committed) {
|
||||
echo "::error::Content version $version does not come after the committed $committed."
|
||||
exit 1
|
||||
}
|
||||
echo "version=$version" >> $env:GITHUB_OUTPUT
|
||||
echo "::notice::Content version: $committed -> $version"
|
||||
|
||||
# A version already released means the input is wrong (or the samples were published already); fail rather
|
||||
# than re-publish different content under it. A re-run on the tagged commit is allowed (completes a partial push).
|
||||
- name: Guard against re-publishing an existing version
|
||||
shell: bash
|
||||
run: |
|
||||
V="${{ steps.version.outputs.version }}"
|
||||
TAG="samples/$V"
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1 && [ -z "$(git tag --points-at HEAD --list "$TAG")" ]; then
|
||||
echo "::error::Content version $V is already released (tag $TAG on another commit). Pick the next number."
|
||||
exit 1
|
||||
fi
|
||||
PUBLISHED=0
|
||||
for id in stride.templates.samples stride.templates.games.starters stride.templates.assetpacks; do
|
||||
if curl -sf "https://api.nuget.org/v3-flatcontainer/$id/index.json" | grep -q "\"$V\""; then
|
||||
PUBLISHED=$((PUBLISHED + 1))
|
||||
fi
|
||||
done
|
||||
if [ "$PUBLISHED" = 3 ]; then
|
||||
echo "::error::Content version $V is already on nuget.org. Pick the next number."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Pack content templates
|
||||
run: |
|
||||
dotnet build build\Stride.build `
|
||||
-t:PackageSamples `
|
||||
-p:StrideSamplesVersion=${{ steps.version.outputs.version }} `
|
||||
-p:StrideSign=${{ inputs.sign }}
|
||||
|
||||
# The engine the packs target is their Stride.Core dependency (the marker the CLI picks templates by). Read it
|
||||
# from the packs themselves and check it is the branch's committed engine version.
|
||||
- name: Check the packages
|
||||
id: check
|
||||
shell: pwsh
|
||||
run: |
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$file = Get-Content sources/shared/SharedAssemblyInfo.cs -Raw
|
||||
$field = { param($name) [regex]::Match($file, "$name = `"([^`"]*)`";").Groups[1].Value }
|
||||
$expected = "$(& $field 'MajorMinor').$(& $field 'Patch')$(& $field 'NuGetVersionSuffix')"
|
||||
foreach ($id in 'Stride.Templates.Samples', 'Stride.Templates.Games.Starters', 'Stride.Templates.AssetPacks') {
|
||||
$pkg = "bin/packages/$id.${{ steps.version.outputs.version }}.nupkg"
|
||||
if (-not (Test-Path $pkg)) {
|
||||
echo "::error::$id.${{ steps.version.outputs.version }}.nupkg was not produced"
|
||||
exit 1
|
||||
}
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $pkg))
|
||||
try {
|
||||
$entry = $zip.Entries | Where-Object { $_.FullName -like '*.nuspec' } | Select-Object -First 1
|
||||
$reader = [System.IO.StreamReader]::new($entry.Open())
|
||||
$nuspec = $reader.ReadToEnd()
|
||||
$reader.Dispose()
|
||||
} finally { $zip.Dispose() }
|
||||
$engine = [regex]::Match($nuspec, '<dependency id="Stride\.Core" version="([^"]*)"').Groups[1].Value
|
||||
if ($engine -ne $expected) {
|
||||
echo "::error::$id targets engine '$engine', expected the branch's $expected"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
echo "engine=$expected" >> $env:GITHUB_OUTPUT
|
||||
echo "::notice title=Target engine::Samples ${{ steps.version.outputs.version }} target engine $expected"
|
||||
"## Samples ${{ steps.version.outputs.version }} for engine $expected" >> $env:GITHUB_STEP_SUMMARY
|
||||
|
||||
# The release-shaped packs are exercised by the smoke tests (pack -> dotnet new install -> instantiate) against
|
||||
# this checkout's build, since the stamped engine may not be published yet. The same properties keep the
|
||||
# tests' own pack identical to the one above.
|
||||
- name: Smoke test
|
||||
run: |
|
||||
dotnet test sources\tools\Stride.Templates.Tests `
|
||||
-c Release `
|
||||
-p:StridePackageBuild=true `
|
||||
-p:StrideSamplesVersion=${{ steps.version.outputs.version }} `
|
||||
--logger "trx;LogFileName=templates.trx" --results-directory TestResults
|
||||
|
||||
- name: Sign
|
||||
if: ${{ inputs.sign }}
|
||||
run: |
|
||||
dotnet build build\Stride.build -t:SignPackage -p:StrideSign=true
|
||||
env:
|
||||
StrideSignTenantId: ${{ secrets.STRIDE_SIGN_TENANT_ID }}
|
||||
StrideSignClientId: ${{ secrets.STRIDE_SIGN_CLIENT_ID }}
|
||||
StrideSignClientSecret: ${{ secrets.STRIDE_SIGN_CLIENT_SECRET }}
|
||||
StrideSignKeyVaultCertificate: ${{ secrets.STRIDE_SIGN_KEYVAULT_CERTIFICATE }}
|
||||
StrideSignKeyVaultName: ${{ secrets.STRIDE_SIGN_KEYVAULT_NAME }}
|
||||
|
||||
- name: Upload packages
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: stride-samples-${{ steps.version.outputs.version }}
|
||||
path: |
|
||||
bin/packages/Stride.Templates.Samples.${{ steps.version.outputs.version }}.nupkg
|
||||
bin/packages/Stride.Templates.Games.Starters.${{ steps.version.outputs.version }}.nupkg
|
||||
bin/packages/Stride.Templates.AssetPacks.${{ steps.version.outputs.version }}.nupkg
|
||||
if-no-files-found: error
|
||||
|
||||
# Surface the version in the run UI even when Deploy is skipped.
|
||||
Version:
|
||||
name: Packaged samples ${{ needs.Package.outputs.version }} for engine ${{ needs.Package.outputs.engine }}
|
||||
needs: Package
|
||||
if: ${{ always() && needs.Package.result == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "::notice title=Packaged samples::${{ needs.Package.outputs.version }} for engine ${{ needs.Package.outputs.engine }}"
|
||||
|
||||
Deploy:
|
||||
name: Deploy ${{ needs.Package.outputs.version }}
|
||||
if: ${{ inputs.deploy && inputs.sign }}
|
||||
needs: Package
|
||||
runs-on: ubuntu-latest # NuGet push doesn't need Windows
|
||||
environment: production # requires manual approval in GitHub settings
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check release permissions
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
ORG=$(echo "${{ github.repository }}" | cut -d/ -f1)
|
||||
TEAM="stride-release-managers"
|
||||
USER="${{ github.actor }}"
|
||||
if ! gh api "orgs/$ORG/teams/$TEAM/memberships/$USER" --silent 2>/dev/null; then
|
||||
echo "::error::User $USER is not a member of $ORG/$TEAM. Deploy requires stride-release-managers team membership."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.GH_PAT }} # push the samples/<version> tag and the SamplesVersion commit
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: stride-samples-${{ needs.Package.outputs.version }}
|
||||
path: bin/packages
|
||||
|
||||
- name: Push to NuGet.org
|
||||
shell: bash
|
||||
run: |
|
||||
# --skip-duplicate: a re-run after a partial push completes the set instead of failing on what went through.
|
||||
for pkg in bin/packages/*.nupkg; do
|
||||
echo "Pushing $(basename "$pkg")..."
|
||||
dotnet nuget push "$pkg" --api-key "$STRIDE_NUGET_API_KEY" --source "https://api.nuget.org/v3/index.json" --timeout 1800 --skip-duplicate
|
||||
done
|
||||
env:
|
||||
STRIDE_NUGET_API_KEY: ${{ secrets.STRIDE_NUGET_API_KEY }}
|
||||
|
||||
- name: Tag release
|
||||
run: |
|
||||
TAG="samples/${{ needs.Package.outputs.version }}"
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG already exists, skipping"
|
||||
else
|
||||
git tag "$TAG" "${{ github.sha }}" # the commit the packages were built from
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
TAG="samples/${{ needs.Package.outputs.version }}"
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
echo "Release $TAG already exists, skipping"
|
||||
else
|
||||
# The samples are released apart from the engine: the changelog is the sample commits since the
|
||||
# previous samples/* tag, so engine commits don't leak into it.
|
||||
PREV=$(git describe --tags --abbrev=0 --match 'samples/*' "$TAG^" 2>/dev/null || true)
|
||||
RANGE="$TAG"
|
||||
[ -n "$PREV" ] && RANGE="$PREV..$TAG"
|
||||
NOTES=$(git log --no-merges --pretty='- %s (%h)' "$RANGE" -- samples ':(exclude)samples/NewGame' ':(exclude)samples/Library')
|
||||
[ -z "$NOTES" ] && NOTES="_No sample changes since the previous samples release._"
|
||||
{
|
||||
echo "For Stride engine ${{ needs.Package.outputs.engine }}."
|
||||
echo ""
|
||||
echo "## What's changed in the Stride samples"
|
||||
echo ""
|
||||
echo "$NOTES"
|
||||
if [ -n "$PREV" ]; then
|
||||
echo ""
|
||||
echo "**Full samples changelog**: ${{ github.server_url }}/${{ github.repository }}/compare/$PREV...$TAG"
|
||||
fi
|
||||
} > samples-notes.md
|
||||
gh release create "$TAG" \
|
||||
--title "Stride samples ${{ needs.Package.outputs.version }}" \
|
||||
--notes-file samples-notes.md \
|
||||
--latest=false \
|
||||
bin/packages/*.nupkg
|
||||
fi
|
||||
|
||||
# Last, once the content is on the feed: the branch now names it, so engines built from here on use it. Goes
|
||||
# to the current branch tip (it may have moved since the build). Needs GH_PAT to push past branch protection,
|
||||
# like the engine's version bump in release-deploy.yml.
|
||||
- name: Commit the content version
|
||||
run: |
|
||||
V="${{ needs.Package.outputs.version }}"
|
||||
FILE=sources/templates/StrideSamplesVersion.props
|
||||
git checkout "${{ github.ref_name }}"
|
||||
git pull --ff-only origin "${{ github.ref_name }}"
|
||||
sed -i "s|<_StrideCommittedSamplesVersion>[^<]*</|<_StrideCommittedSamplesVersion>$V</|" "$FILE"
|
||||
if ! grep -q "<_StrideCommittedSamplesVersion>$V</_StrideCommittedSamplesVersion>" "$FILE"; then
|
||||
echo "::error::Could not set the content version $V in $FILE. The samples are published; set it by hand."
|
||||
exit 1
|
||||
fi
|
||||
if git diff --quiet -- "$FILE"; then
|
||||
echo "The content version is already $V, nothing to commit"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
# Commit only the version file (never -a), so an unexpected working-tree change can't ride along.
|
||||
git commit -m "Samples: content version $V (released samples/$V)" -- "$FILE"
|
||||
git pull --rebase origin "${{ github.ref_name }}"
|
||||
git push origin "HEAD:${{ github.ref_name }}"
|
||||
|
||||
- name: Publish summary
|
||||
if: success()
|
||||
shell: bash
|
||||
run: |
|
||||
V="${{ needs.Package.outputs.version }}"
|
||||
{
|
||||
echo "## Published Stride samples $V"
|
||||
echo ""
|
||||
echo "For engine ${{ needs.Package.outputs.engine }}."
|
||||
echo ""
|
||||
for id in Stride.Templates.Samples Stride.Templates.Games.Starters Stride.Templates.AssetPacks; do
|
||||
echo "- [$id](https://www.nuget.org/packages/$id/$V)"
|
||||
done
|
||||
echo "- [GitHub Release](${{ github.server_url }}/${{ github.repository }}/releases/tag/samples/$V)"
|
||||
echo ""
|
||||
echo "SamplesVersion is now $V on ${{ github.ref_name }}; engines built from there use this content."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -27,10 +27,6 @@ on:
|
||||
description: Build prerequisites installer
|
||||
default: true
|
||||
type: boolean
|
||||
version-suffix:
|
||||
description: Prerelease suffix, no leading dash (e.g. "beta" -> 4.4.123-beta). Empty = stable.
|
||||
default: ''
|
||||
type: string
|
||||
deploy:
|
||||
description: Deploy to NuGet.org and create GitHub Release
|
||||
default: false
|
||||
@@ -133,7 +129,6 @@ jobs:
|
||||
-p:StridePlatforms=$($platforms.Replace(';','%3B')) `
|
||||
-p:StrideGraphicsApiDependentBuildAll=true `
|
||||
-p:StrideSign=${{ inputs.sign }} `
|
||||
-p:StrideVersionSuffix=${{ inputs.version-suffix }} `
|
||||
-p:StrideBuildPrerequisitesInstaller=${{ inputs.build-prerequisites-installer }} `
|
||||
-p:StrideSentryDsn=${{ vars.STRIDE_SENTRY_DSN }} `
|
||||
-p:StrideSentryEnvironment=${{ vars.STRIDE_SENTRY_DSN != '' && 'release' || '' }} `
|
||||
|
||||
@@ -77,6 +77,7 @@ concurrency:
|
||||
jobs:
|
||||
Android-Tests-Game:
|
||||
name: Android (Vulkan, ${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }})
|
||||
if: github.event_name != 'schedule' || vars.STRIDE_ENABLE_SCHEDULED_CI == 'true'
|
||||
# Linux host: KVM-accelerated x86_64 Android emulator is the only path that runs in a
|
||||
# reasonable amount of time on a hosted runner. macOS-14+ Apple Silicon runners don't
|
||||
# support nested virt for AOSP emulator; older macOS-13 Intel HAXM is being retired.
|
||||
|
||||
@@ -98,6 +98,7 @@ jobs:
|
||||
STRIDE_GRAPHICS_SOFTWARE_RENDERING: "1"
|
||||
STRIDE_TESTS_RENDERDOC: "error"
|
||||
STRIDE_MAX_PARALLELISM: "8"
|
||||
STRIDE_VULKAN_SYNC_VALIDATION: ${{ matrix.graphics-api == 'Vulkan' && '1' || '' }}
|
||||
# Crash dump collection (covers 3 crash types) — all dumps land under TestResults/
|
||||
# alongside the .trx logs and the blame-hang dumps from `dotnet test`.
|
||||
STRIDE_TESTS_CRASH_DUMPS: "1" # Type 1: SEHException logging + minidump via FirstChanceException
|
||||
|
||||
@@ -59,14 +59,14 @@ if (string.IsNullOrEmpty(solution))
|
||||
|
||||
if (string.IsNullOrEmpty(version))
|
||||
{
|
||||
// Package versions use the committed MajorMinor.Patch (see StrideVersionTasks.cs). The -devN suffix comes from
|
||||
// the generated overlay when present.
|
||||
// Dev package versions are the committed MajorMinor.Patch without its prerelease suffix (see StrideVersionTasks.cs),
|
||||
// plus the -devN suffix from the generated overlay when present.
|
||||
var generatedFile = Path.Combine(strideRoot, "sources", "shared", "SharedAssemblyInfo.Generated.cs");
|
||||
var plainFile = Path.Combine(strideRoot, "sources", "shared", "SharedAssemblyInfo.cs");
|
||||
var plainText = File.ReadAllText(plainFile);
|
||||
var mmMatch = Regex.Match(plainText, @"MajorMinor\s*=\s*""([^""]+)""");
|
||||
var patchMatch = Regex.Match(plainText, @"\bPatch\s*=\s*""([^""]+)""");
|
||||
var suffixMatch = Regex.Match(File.ReadAllText(File.Exists(generatedFile) ? generatedFile : plainFile), @"NuGetVersionSuffix\s*=\s*""([^""]*)""");
|
||||
var suffixMatch = File.Exists(generatedFile) ? Regex.Match(File.ReadAllText(generatedFile), @"NuGetVersionSuffix\s*=\s*""([^""]*)""") : Match.Empty;
|
||||
if (!mmMatch.Success || !patchMatch.Success) throw new Exception("Could not determine version from SharedAssemblyInfo");
|
||||
version = mmMatch.Groups[1].Value + "." + patchMatch.Groups[1].Value + (suffixMatch.Success ? suffixMatch.Groups[1].Value : "");
|
||||
}
|
||||
@@ -232,8 +232,11 @@ if (freshPackages.Length == 0)
|
||||
// fell back to whatever stale package the NuGet cache still held. Every packed project that had a stub before
|
||||
// must have produced one now, else stop and leave the feed as it was.
|
||||
var fresh = freshPackages.Select(Path.GetFileName).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
// The content template packs (Stride.Templates.Samples / .Games.Starters / .AssetPacks) are only packed on request
|
||||
// (StridePackContentTemplates), so a stamp that lists them from such a build says nothing about this pack.
|
||||
var missing = (adopt.Length > 0 ? new List<ProjectInfo>() : projects)
|
||||
.Select(StubName)
|
||||
.Where(name => !name.StartsWith("Stride.Templates.", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(name => previousStamp.Contains(name, StringComparer.OrdinalIgnoreCase) && !fresh.Contains(name))
|
||||
.ToList();
|
||||
if (missing.Count > 0)
|
||||
|
||||
+92
-38
@@ -1,23 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
In-repo sample version management. Standalone entry point - run directly, e.g.
|
||||
dotnet msbuild build/Stride.Samples.build -t:UpgradeSamplesVersion. See plans/samples-version-bump.md.
|
||||
dotnet msbuild build/Stride.Samples.build -t:PrepareSamplesForRelease. See docs/build/versioning.md.
|
||||
|
||||
Samples are committed at a clean release version (e.g. 4.4.0), but locally only the per-checkout dev
|
||||
packages exist (4.4.0-devN). These targets switch the sample csprojs between the clean and dev version
|
||||
by REAL on-disk edits (no evaluation-time flip), so GameStudio / restore read the version directly:
|
||||
Two versions meet here:
|
||||
- the ENGINE version the sample csprojs reference (SamplesEngineVersion, the committed release version such as 4.4.0 or
|
||||
4.4.0-beta1; locally only the per-checkout dev packages 4.4.0-devN exist, so the targets switch the csprojs between the two by REAL on-disk
|
||||
edits, no evaluation-time flip);
|
||||
- the CONTENT version of the template packs made from the samples (StrideSamplesVersion, from
|
||||
sources/templates/StrideSamplesVersion.props: the exact published version an engine names). Only
|
||||
release-samples.yml changes it, after publishing.
|
||||
|
||||
UpgradeSamplesVersion - bump from the committed version through the package upgrader: migrate assets,
|
||||
rewrite refs to this checkout's dev build (4.4.0-devN), restore. Leaves the
|
||||
csprojs at the dev version so they open in GameStudio.
|
||||
SamplesToDevVersion - switch already-bumped samples (at the clean version) to the dev build, to
|
||||
re-open them on a worktree later.
|
||||
SamplesToReleaseVersion - switch the dev version back to the clean release version. Run before committing.
|
||||
PrepareSamplesForRelease - bring the samples up to this engine: SamplesToDevEngine, UpgradeSamples,
|
||||
SamplesToReleaseEngine, pack the content templates as this checkout's -devN, print
|
||||
what is left to do by hand.
|
||||
UpgradeSamples - run the samples through the package upgraders: migrate assets, rewrite refs to this
|
||||
checkout's dev build (4.4.0-devN), restore. Leaves the csprojs at the dev version so
|
||||
they open in GameStudio.
|
||||
SamplesToDevEngine - switch already-upgraded samples (at the clean version) to the dev build, to
|
||||
re-open them on a worktree later.
|
||||
SamplesToReleaseEngine - switch the dev version back to the clean release version. Run before committing.
|
||||
PackContentTemplates - pack Stride.Templates.Samples / .Games.Starters / .AssetPacks (a normal build never does).
|
||||
CleanContentTemplates - remove this checkout's local content packs, so the published ones resolve again
|
||||
(a build with StridePackContentTemplates off also does it).
|
||||
-->
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- StrideRoot self-default: this file is run standalone, e.g.
|
||||
dotnet msbuild build/Stride.Samples.build -t:UpgradeSamplesVersion. -->
|
||||
dotnet msbuild build/Stride.Samples.build -t:UpgradeSamples. -->
|
||||
<PropertyGroup>
|
||||
<StrideRoot Condition="'$(StrideRoot)' == ''">$(MSBuildThisFileDirectory)..\</StrideRoot>
|
||||
</PropertyGroup>
|
||||
@@ -31,6 +41,18 @@
|
||||
<Import Project="$(StrideRoot)sources\targets\Stride.WorktreeVersion.targets"
|
||||
Condition="'$(_StrideWorktreeVersionTargetsImported)' != 'true'" />
|
||||
|
||||
<!-- The content version authority (StrideSamplesVersion, _StrideCommittedSamplesVersion), shared with the
|
||||
template pack (Stride.Templates.Common.targets). -->
|
||||
<Import Project="$(StrideRoot)sources\templates\StrideSamplesVersion.props" />
|
||||
|
||||
<!-- The content template projects (StrideTemplateContentVersioned=true); Stride.Templates.Games is
|
||||
engine-versioned and packed by every build. -->
|
||||
<ItemGroup>
|
||||
<_StrideContentTemplateProject Include="$(StrideRoot)sources\templates\Stride.Templates.Games.Starters\Stride.Templates.Games.Starters.csproj" />
|
||||
<_StrideContentTemplateProject Include="$(StrideRoot)sources\templates\Stride.Templates.Samples\Stride.Templates.Samples.csproj" />
|
||||
<_StrideContentTemplateProject Include="$(StrideRoot)sources\templates\Stride.Templates.AssetPacks\Stride.Templates.AssetPacks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Rewrites Stride.* PackageReference Versions equal to SourceVersion -> TargetVersion (a real csproj
|
||||
edit). The exact source-version match is the whole filter: only refs at the expected version are
|
||||
touched, so $-placeholder versions (e.g. $EngineVersion$) and third-party Stride.*-prefixed packages
|
||||
@@ -59,23 +81,24 @@
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<!-- Resolves SampleVersion (clean release, suffix off) + SampleDevVersion (clean + worktree
|
||||
suffix = the locally-published dev version) + the concrete sample csproj set (all under
|
||||
samples/ minus the $EngineVersion$ template sources NewGame/Library). -->
|
||||
<Target Name="_ResolveSampleVersions" DependsOnTargets="StrideEnsureWorktreeVersion">
|
||||
<!-- Resolves SamplesEngineVersion (the committed release version) + SamplesDevEngineVersion (MajorMinor.Patch +
|
||||
worktree suffix = the locally-published dev version, which never carries the committed -betaN) + the concrete sample csproj set (all under
|
||||
samples/ minus the $EngineVersion$ template sources NewGame/Library, the set release-deploy.yml
|
||||
checks for changes since the content version was set). -->
|
||||
<Target Name="_ResolveSamplesEngineVersions" DependsOnTargets="StrideEnsureWorktreeVersion">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
|
||||
</PropertyGroup>
|
||||
<MakeDir Directories="$(StrideRoot)bin\obj" Condition="'$(SampleVersion)' == ''" />
|
||||
<StrideGitVersion Condition="'$(SampleVersion)' == ''"
|
||||
<MakeDir Directories="$(StrideRoot)bin\obj" Condition="'$(SamplesEngineVersion)' == ''" />
|
||||
<StrideGitVersion Condition="'$(SamplesEngineVersion)' == ''"
|
||||
RootDirectory="$(StrideRoot)"
|
||||
VersionFile="sources\shared\SharedAssemblyInfo.cs"
|
||||
GeneratedVersionFile="bin\obj\SampleVersionProbe.cs">
|
||||
<Output TaskParameter="NuGetVersion" PropertyName="SampleVersion" />
|
||||
<Output TaskParameter="NuGetVersion" PropertyName="SamplesEngineVersion" />
|
||||
</StrideGitVersion>
|
||||
<Error Condition="'$(SampleVersion)' == ''" Text="Could not resolve a sample version. Pass -p:SampleVersion=." />
|
||||
<Error Condition="'$(SamplesEngineVersion)' == ''" Text="Could not resolve a sample version. Pass -p:SamplesEngineVersion=." />
|
||||
<PropertyGroup>
|
||||
<SampleDevVersion>$(SampleVersion)$(StrideWorktreeSuffix)</SampleDevVersion>
|
||||
<SamplesDevEngineVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(SamplesEngineVersion)', '-.*$', ''))$(StrideWorktreeSuffix)</SamplesDevEngineVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<_StrideSampleCsproj Include="$(StrideRoot)samples\**\*.csproj" />
|
||||
@@ -85,27 +108,27 @@
|
||||
</Target>
|
||||
|
||||
<!-- Switch already-bumped samples (committed at the clean version) to this checkout's dev build, e.g.
|
||||
before opening them in GameStudio on a worktree. Real csproj edits; run SamplesToReleaseVersion
|
||||
before committing. A no-op on samples not at the clean version (run UpgradeSamplesVersion first). -->
|
||||
<Target Name="SamplesToDevVersion" DependsOnTargets="_ResolveSampleVersions">
|
||||
<SetStrideVersionInProjects Projects="@(_StrideSampleCsproj)" SourceVersion="$(SampleVersion)" TargetVersion="$(SampleDevVersion)" />
|
||||
<Message Importance="high" Text="Samples set to dev version $(SampleDevVersion)." />
|
||||
before opening them in GameStudio on a worktree. Real csproj edits; run SamplesToReleaseEngine
|
||||
before committing. A no-op on samples not at the clean version (run UpgradeSamples first). -->
|
||||
<Target Name="SamplesToDevEngine" DependsOnTargets="_ResolveSamplesEngineVersions">
|
||||
<SetStrideVersionInProjects Projects="@(_StrideSampleCsproj)" SourceVersion="$(SamplesEngineVersion)" TargetVersion="$(SamplesDevEngineVersion)" />
|
||||
<Message Importance="high" Text="Samples set to dev version $(SamplesDevEngineVersion)." />
|
||||
</Target>
|
||||
|
||||
<!-- Switch samples from the dev build back to the committed clean release version. Run before committing. -->
|
||||
<Target Name="SamplesToReleaseVersion" DependsOnTargets="_ResolveSampleVersions">
|
||||
<SetStrideVersionInProjects Projects="@(_StrideSampleCsproj)" SourceVersion="$(SampleDevVersion)" TargetVersion="$(SampleVersion)" />
|
||||
<Message Importance="high" Text="Samples set to release version $(SampleVersion)." />
|
||||
<Target Name="SamplesToReleaseEngine" DependsOnTargets="_ResolveSamplesEngineVersions">
|
||||
<SetStrideVersionInProjects Projects="@(_StrideSampleCsproj)" SourceVersion="$(SamplesDevEngineVersion)" TargetVersion="$(SamplesEngineVersion)" />
|
||||
<Message Importance="high" Text="Samples set to release version $(SamplesEngineVersion)." />
|
||||
</Target>
|
||||
|
||||
<!-- Bump the in-repo samples through the real package upgraders: each sample upgrades from its committed
|
||||
version to this checkout's dev build - migrate assets, rewrite references to 4.4.0-devN, restore from
|
||||
the local dev feed. The csprojs are left at the dev version so they open in GameStudio; run
|
||||
SamplesToReleaseVersion to finalize at the clean version before committing. SampleVersion defaults to
|
||||
the clean next-release number; -p:SampleVersion= overrides (e.g. a -beta). -p:SamplePackages= limits
|
||||
the migrated .sdpkg set. -->
|
||||
<Target Name="UpgradeSamplesVersion" DependsOnTargets="_ResolveSampleVersions">
|
||||
<Message Importance="high" Text="UpgradeSamplesVersion: migrate -> dev=$(SampleDevVersion) ($(Configuration)); run SamplesToReleaseVersion to finalize at $(SampleVersion)" />
|
||||
SamplesToReleaseEngine to finalize at the clean version before committing. SamplesEngineVersion defaults to
|
||||
the committed release version, suffix included; -p:SamplesEngineVersion= overrides. -p:SamplePackages= limits
|
||||
the migrated .sdpkg set. Part of PrepareSamplesForRelease; on its own it does not touch the content version. -->
|
||||
<Target Name="UpgradeSamples" DependsOnTargets="_ResolveSamplesEngineVersions">
|
||||
<Message Importance="high" Text="UpgradeSamples: migrate -> dev=$(SamplesDevEngineVersion) ($(Configuration)); run SamplesToReleaseEngine to finalize at $(SamplesEngineVersion)" />
|
||||
|
||||
<!-- Build the source CompilerApp and locate its xplat dll (net*.0 excludes the -windows TFM). -->
|
||||
<MSBuild Projects="$(StrideRoot)sources\assets\Stride.AssetCompiler\Stride.AssetCompiler.csproj"
|
||||
@@ -122,14 +145,45 @@
|
||||
<PropertyGroup>
|
||||
<SampleUpgradeTarget Condition="'$(SampleUpgradeTarget)' == ''">$(StrideRoot)samples\StrideSamples.slnx</SampleUpgradeTarget>
|
||||
</PropertyGroup>
|
||||
<Message Importance="high" Text="UpgradeSamplesVersion: upgrading $(SampleUpgradeTarget)" />
|
||||
<Message Importance="high" Text="UpgradeSamples: upgrading $(SampleUpgradeTarget)" />
|
||||
<Exec WorkingDirectory="$(StrideRoot)"
|
||||
Command="dotnet "@(_StrideCompilerAppDll)" upgrade "$(SampleUpgradeTarget)"" />
|
||||
<Message Importance="high" Text="UpgradeSamples: done (samples at $(SamplesDevEngineVersion))" />
|
||||
</Target>
|
||||
|
||||
<!-- Record the authority for the content-versioned template packages (Samples, Starters). -->
|
||||
<WriteLinesToFile File="$(StrideRoot)sources\templates\StrideSamplesVersion.props" Overwrite="true" WriteOnlyWhenDifferent="true"
|
||||
Lines="<Project><PropertyGroup><StrideSamplesVersion>$(SampleVersion)</StrideSamplesVersion></PropertyGroup></Project>" />
|
||||
<Message Importance="high" Text="UpgradeSamplesVersion: done (samples at $(SampleDevVersion)); StrideSamplesVersion=$(SampleVersion)" />
|
||||
<!-- Pack the content templates (Stride.Templates.Samples / .Games.Starters / .AssetPacks) at StrideSamplesVersion,
|
||||
worktree-suffixed on a local build (4.4.0-dev2), exact on CI / release builds (StridePackageBuild=true also
|
||||
selects the fully preprocessed pack mode). A build, not a pack: these projects pack on build, and in that mode
|
||||
'dotnet pack' skips the build, so the preprocessor they reference would not be built on a fresh checkout. The
|
||||
engine projects they reference (the Stride.Core marker, the preprocessor) auto-pack next to them, as in any
|
||||
build; a release only picks up the content packs. -->
|
||||
<Target Name="PackContentTemplates" DependsOnTargets="_ResolveSamplesEngineVersions">
|
||||
<PropertyGroup>
|
||||
<_StrideContentPackArgs>-c $(Configuration) -p:StridePackContentTemplates=true -p:StrideSamplesVersion=$(StrideSamplesVersion)</_StrideContentPackArgs>
|
||||
<_StrideContentPackArgs Condition="'$(StridePackageBuild)' == 'true'">$(_StrideContentPackArgs) -p:StridePackageBuild=true</_StrideContentPackArgs>
|
||||
</PropertyGroup>
|
||||
<Exec Command="dotnet build "%(_StrideContentTemplateProject.Identity)" $(_StrideContentPackArgs)" WorkingDirectory="$(StrideRoot)" />
|
||||
</Target>
|
||||
|
||||
<!-- Remove this checkout's local content packs (bin/packages, the nugetdev feed, the global cache), so the
|
||||
published content version resolves again in Game Studio and the CLI. -->
|
||||
<Target Name="CleanContentTemplates">
|
||||
<MSBuild Projects="@(_StrideContentTemplateProject)" Targets="CleanContentTemplates" />
|
||||
</Target>
|
||||
|
||||
<!-- Brings the samples up to this engine, as far as a tool can: UpgradeSamples (real upgraders), SamplesToReleaseEngine,
|
||||
then a -devN pack of the content to try them. Then the manual part the tool cannot judge: open the samples in
|
||||
Game Studio, fix what the upgraders left, commit them in a normal PR. The content version is not touched here:
|
||||
release-samples.yml takes the number, publishes the samples and only then commits SamplesVersion. -->
|
||||
<Target Name="PrepareSamplesForRelease" DependsOnTargets="_ResolveSamplesEngineVersions">
|
||||
<Message Importance="high" Text="PrepareSamplesForRelease: samples referencing engine $(SamplesEngineVersion)" />
|
||||
|
||||
<!-- Samples already at the engine's number (a content-only release) are not rewritten by the upgraders, so point
|
||||
them at this checkout's dev build first: the committed clean version is not a package that exists locally
|
||||
(or worse, it resolves to a stale cached copy). A no-op for samples still at an older engine version. -->
|
||||
<CallTarget Targets="SamplesToDevEngine;UpgradeSamples;SamplesToReleaseEngine;PackContentTemplates" />
|
||||
|
||||
<Message Importance="high" Text="%0APrepareSamplesForRelease: samples upgraded and packed locally as $(StrideSamplesVersion)$(StrideWorktreeSuffix). Left to do:%0A 1. Open the samples in Game Studio (SamplesToDevEngine points them at this checkout's engine again) and fix what the upgraders left.%0A 2. Run SamplesToReleaseEngine, commit the samples and merge them with a normal PR.%0A 3. Publish them: release-samples.yml with the next content version; it commits SamplesVersion once published.%0A 4. CleanContentTemplates when this checkout should resolve the published content again." />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
+17
-8
@@ -113,11 +113,8 @@ Example: dotnet build build/Stride.build -t:Package
|
||||
<NuGetPackage Include="..\bin\packages\*.nupkg"/>
|
||||
<!-- Remove GameStudio (pushed in a second stage) -->
|
||||
<NuGetPackage Remove="@(NuGetPackage)" Condition="$([System.String]::Copy('%(Filename)').Contains('Stride.GameStudio'))"/>
|
||||
<!-- Remove Samples.Templates (we use -SkipDuplicate since version is rarely bumped) -->
|
||||
<NuGetPackage Remove="@(NuGetPackage)" Condition="$([System.String]::Copy('%(Filename)').Contains('Stride.Samples.Templates'))"/>
|
||||
</ItemGroup>
|
||||
<Exec Command='"$(NuGetPath)" push %(NuGetPackage.Identity) -ApiKey $(StrideStoreApiKey) -Source $(StrideStoreUrl) -Timeout 1800 -NoSymbols -SkipDuplicate'/>
|
||||
<Exec Command='"$(NuGetPath)" push ..\bin\packages\Stride.Samples.Templates*.nupkg -ApiKey $(StrideStoreApiKey) -Source $(StrideStoreUrl) -Timeout 1800 -NoSymbols -SkipDuplicate'/>
|
||||
<!-- Push GameStudio so that dependencies are already uploaded -->
|
||||
<Exec Command='"$(NuGetPath)" push ..\bin\packages\Stride.GameStudio*.nupkg -ApiKey $(StrideStoreApiKey) -Source $(StrideStoreUrl) -Timeout 1800 -NoSymbols'/>
|
||||
</Target>
|
||||
@@ -151,12 +148,12 @@ Example: dotnet build build/Stride.build -t:Package
|
||||
|
||||
<!-- Create SharedAssemblyInfo.Generated.cs with the committed version + git metadata (the Stride SDK swaps
|
||||
it in for SharedAssemblyInfo.cs when building the packages). -->
|
||||
<StrideGitVersion RootDirectory="$(StrideRoot)" VersionFile="sources\shared\SharedAssemblyInfo.cs" GeneratedVersionFile="sources\shared\SharedAssemblyInfo.Generated.cs" SuffixOverride="$(StrideVersionSuffix)">
|
||||
<StrideGitVersion RootDirectory="$(StrideRoot)" VersionFile="sources\shared\SharedAssemblyInfo.cs" GeneratedVersionFile="sources\shared\SharedAssemblyInfo.Generated.cs">
|
||||
<Output TaskParameter="NuGetVersion" PropertyName="StridePackageVersion"/>
|
||||
</StrideGitVersion>
|
||||
|
||||
<Message Text="Done preparing build for version $(StridePackageVersion)"/>
|
||||
<Exec Command="echo **Version:** $(StridePackageVersion) >> $(GITHUB_STEP_SUMMARY)" Condition="'$(GITHUB_STEP_SUMMARY)' != ''" IgnoreExitCode="true" />
|
||||
<Exec Command="echo **Engine version:** $(StridePackageVersion) >> $(GITHUB_STEP_SUMMARY)" Condition="'$(GITHUB_STEP_SUMMARY)' != ''" IgnoreExitCode="true" />
|
||||
</Target>
|
||||
<PropertyGroup>
|
||||
<_StridePackageTargetPrerequisitesInstaller Condition="'$(StrideBuildPrerequisitesInstaller)' == 'true'">PrerequisitesInstaller</_StridePackageTargetPrerequisitesInstaller>
|
||||
@@ -199,9 +196,21 @@ Example: dotnet build build/Stride.build -t:Package
|
||||
|
||||
<Target Name="PackageCli" DependsOnTargets="BuildPackageCli;SignPackageCli" />
|
||||
|
||||
<!-- In-repo sample version management (SamplesToDevVersion / SamplesToReleaseVersion /
|
||||
UpgradeSamplesVersion) is standalone in build/Stride.Samples.build - run it directly, e.g.
|
||||
dotnet msbuild build/Stride.Samples.build -t:UpgradeSamplesVersion. See plans/samples-version-bump.md. -->
|
||||
<!--
|
||||
Content template packs (Stride.Templates.Samples / .Games.Starters / .AssetPacks)
|
||||
Versioned by sources/templates/StrideSamplesVersion.props, published on their own schedule by
|
||||
.github/workflows/release-samples.yml; the engine release never packs them. PackageSamples produces the
|
||||
release-shaped (fully preprocessed) packs into bin/packages, stamped for this branch's engine version (the committed
|
||||
one, with its suffix); SignPackage then signs them.
|
||||
-->
|
||||
<Target Name="PackageSamples" DependsOnTargets="PreparePackageBuild">
|
||||
<MSBuild Projects="$(StrideRoot)build\Stride.Samples.build" Targets="PackContentTemplates"
|
||||
Properties="Configuration=Release;StridePackageBuild=true" />
|
||||
</Target>
|
||||
|
||||
<!-- In-repo sample version management (SamplesToDevEngine / SamplesToReleaseEngine / PrepareSamplesForRelease /
|
||||
CleanContentTemplates) is standalone in build/Stride.Samples.build - run it directly, e.g.
|
||||
dotnet msbuild build/Stride.Samples.build -t:PrepareSamplesForRelease. See docs/build/versioning.md. -->
|
||||
|
||||
<Target Name="RestoreInternal">
|
||||
<MSBuild Targets="Restore" Projects="$(StrideSolution)" Properties="StrideGraphicsApiDependentBuildAll=$(StrideGraphicsApiDependentBuildAll);StrideGraphicsApis=$(StrideGraphicsApis);StridePlatforms=$(StridePlatforms)" />
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
</Folder>
|
||||
<Folder Name="/00-Config/">
|
||||
<File Path="../sources/shared/SharedAssemblyInfo.cs" />
|
||||
<File Path="../sources/templates/StrideSamplesVersion.props" />
|
||||
</Folder>
|
||||
<Folder Name="/00-Localization/">
|
||||
<File Path="../sources/localization/Stride.Assets.Presentation.pot" />
|
||||
@@ -200,6 +201,7 @@
|
||||
<Project Path="../sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/72-StrideSamples.Templates/">
|
||||
<File Path="../sources/templates/StrideSamplesVersion.props" />
|
||||
<Project Path="../sources/engine/Stride.Games.AutoTesting/Stride.Games.AutoTesting.csproj" />
|
||||
<Project Path="../sources/templates/Stride.Templates.AssetPacks/Stride.Templates.AssetPacks.csproj" />
|
||||
<Project Path="../sources/templates/Stride.Templates.Games.Starters/Stride.Templates.Games.Starters.csproj" />
|
||||
|
||||
Vendored
+37
-13
@@ -10,7 +10,7 @@ The source of truth is [`sources/shared/SharedAssemblyInfo.cs`](../../sources/sh
|
||||
|---|---|---|
|
||||
| `PublicVersion` | `4.4.0` | 3-part `major.minor.patch` display/package version. Committed and bumped per release (see below). |
|
||||
| `AssemblyVersion` | `4.4.0.0` | Assembly binding identity, pinned per `major.minor` so the git height never churns it. Bump together with `PublicVersion`'s `major.minor`. |
|
||||
| `NuGetVersionSuffix` | `` / `-beta` / `-dev2` | Prerelease tag. Empty for a stable release; set by the worktree system (`-devN`) or a release prerelease (`-beta`). |
|
||||
| `NuGetVersionSuffix` | `` / `-beta1` / `-dev2` | Prerelease tag. Committed: empty for a stable release, `-betaN` before a prerelease (only package builds apply it). Dev builds replace it with the worktree suffix (`-devN`). |
|
||||
| `BuildMetadata` | `+g<sha>` | Set during package builds. |
|
||||
|
||||
`NuGetVersion = PublicVersion + NuGetVersionSuffix`; `StrideVersion.NuGetVersion` (the compiled const) is what the package upgrader stamps into consumer projects.
|
||||
@@ -25,6 +25,7 @@ Two rules:
|
||||
|
||||
- **Bump per release.** The release pipeline refuses to publish a version whose `releases/<version>` tag already exists on another commit (see [Release flow](#release-flow)), so a forgotten bump fails the deploy rather than silently re-publishing.
|
||||
- **A format change ⇒ a numeric (`Patch`) bump.** Asset upgraders gate on the *numeric* version and ignore the prerelease suffix (`-beta1`, `-dev3`, custom), so a format change must advance the number (e.g. `4.4.0` → `4.4.1`) for the gate to fire. Successive prereleases without a format change can stay at the same number (`4.4.0-beta1`, `4.4.0-beta2`).
|
||||
- **A prerelease is a committed suffix.** Commit `NuGetVersionSuffix = "-beta2"` before running the release, like a `Patch` bump, and `""` before the stable release. The release workflows have no suffix input. Only package builds apply the committed suffix: dev and CI builds stay at `MajorMinor.Patch` (+ `-devN`), so all the betas of one version share one dev version (and one NuGet cache slot per checkout).
|
||||
|
||||
## Per-checkout dev versions (`-devN`)
|
||||
|
||||
@@ -45,7 +46,7 @@ Multiple checkouts of Stride on one machine (git worktrees *or* independent clon
|
||||
|
||||
1. **Version** = the committed value in `SharedAssemblyInfo.cs` (`StrideGitVersion` reads it and adds `+g<sha>`).
|
||||
2. **Package** builds all platforms unsigned → `bin/packages/*.nupkg`.
|
||||
3. **Deploy** (only when `sign && deploy`) pushes to nuget.org, then **creates and pushes the `releases/<version>` tag** — so the tag is a *consequence* of a successful release; re-publishing a not-bumped version is refused because its tag already exists (the forget-to-bump guard). Idempotent: `--skip-duplicate`, plus content-versioned template packages are pre-checked against the nuget flat-container index and skipped if already published.
|
||||
3. **Deploy** (only when `sign && deploy`) checks the content version the engine names is published (see [Samples & template package versions](#samples--template-package-versions)), pushes to nuget.org, then **creates and pushes the `releases/<version>` tag** — so the tag is a *consequence* of a successful release; re-publishing a not-bumped version is refused because its tag already exists (the forget-to-bump guard). Idempotent: `--skip-duplicate`.
|
||||
4. **Bump** (stable deploys only, unless `bump-version: false`) — after publishing, the workflow commits `Patch+1` to the branch and pushes it, so the branch opens the next dev version. Skipped for prereleases (they keep the same number) and idempotent re-runs (tag already existed).
|
||||
|
||||
So you just dispatch `release.yml` on the branch: it publishes the committed version, tags it, and (for a stable release) advances the source to the next patch — no manual version-bump commit. You only edit the version by hand to start a new major/minor cycle or a beta. (The bump-commit push needs the checkout token — `GH_PAT` — to have push rights to the branch, i.e. bypass branch protection.)
|
||||
@@ -62,26 +63,49 @@ Install: `dotnet tool install -g Stride.Cli`.
|
||||
|
||||
## Samples & template package versions
|
||||
|
||||
The in-repo samples are committed referencing a **clean release version** (e.g. `4.4.0`) — which is typically still *unreleased* at commit time, since the bump rides the release that publishes it (the matching packages only appear on nuget.org once `release.yml` deploys). Locally, only the `-devN` packages exist. So to build/run/edit a sample in your checkout (including opening it in GameStudio) you switch it to the local dev version, and switch back before committing — standalone targets in [`build/Stride.Samples.build`](../../build/Stride.Samples.build):
|
||||
The in-repo samples are committed referencing the **release version**, the committed one with its suffix (e.g. `4.4.0` or `4.4.0-beta2`) — which is typically still *unreleased* at commit time, since the bump rides the release that publishes it (the matching packages only appear on nuget.org once `release.yml` deploys). Locally, only the `-devN` packages exist. So to build/run/edit a sample in your checkout (including opening it in GameStudio) you switch it to the local dev version, and switch back before committing — standalone targets in [`build/Stride.Samples.build`](../../build/Stride.Samples.build):
|
||||
|
||||
```bash
|
||||
dotnet msbuild build/Stride.Samples.build -t:SamplesToDevVersion # before editing/building locally
|
||||
dotnet msbuild build/Stride.Samples.build -t:SamplesToDevEngine # before editing/building locally
|
||||
# ... edit / build / run (e.g. in GameStudio) ...
|
||||
dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseVersion # before committing
|
||||
dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseEngine # before committing
|
||||
```
|
||||
|
||||
These do **real csproj edits** via `SetStrideVersionInProjects`: `SamplesToDevVersion` rewrites every `Stride.*` `PackageReference` from the committed clean version to this checkout's dev build (e.g. `4.4.0` → `4.4.0-dev2`) so restore/build/GameStudio resolve the local packages; `SamplesToReleaseVersion` rewrites them back. So while you work, the sample's csproj shows as modified — finalize with `SamplesToReleaseVersion` before committing. There is no eval-time override or generated props: the version in the csproj is always the real, resolvable one.
|
||||
These do **real csproj edits** via `SetStrideVersionInProjects`: `SamplesToDevEngine` rewrites every `Stride.*` `PackageReference` from the committed clean version to this checkout's dev build (e.g. `4.4.0` → `4.4.0-dev2`) so restore/build/GameStudio resolve the local packages; `SamplesToReleaseEngine` rewrites them back. So while you work, the sample's csproj shows as modified — finalize with `SamplesToReleaseEngine` before committing. There is no eval-time override or generated props: the version in the csproj is always the real, resolvable one.
|
||||
|
||||
The release-time bump runs every sample through the **real package upgraders**: each upgrades from its committed version (e.g. `4.3.x`) to this checkout's dev build — migrating assets to the current format, rewriting `Stride.*` to `4.4.0-devN`, and restoring from the local dev feed. It leaves the csprojs at the dev version (so they still open in GameStudio) and records the content-version authority (`StrideSamplesVersion.props`); run `SamplesToReleaseVersion` to finalize at the clean version before committing:
|
||||
### Template package versions
|
||||
|
||||
Two kinds of template packages ([`Stride.Templates.Common.targets`](../../sources/templates/Stride.Templates.Common.targets)):
|
||||
|
||||
- `Stride.Templates.Games` (NewGame + Library) is **engine-versioned** (`$(StrideNuGetVersion)`): small, packed by every build, published with every engine release.
|
||||
- `Stride.Templates.Samples`, `Stride.Templates.Games.Starters` and `Stride.Templates.AssetPacks` are **content-versioned**: ~730 MB of samples that rarely change, so they have their own version and their own release. The version is `StrideSamplesVersion` in [`StrideSamplesVersion.props`](../../sources/templates/StrideSamplesVersion.props), kept out of `SharedAssemblyInfo.cs` so a samples release rebuilds only `Stride.Assets` (which embeds the file as a resource, outside its reference assembly, so its dependents don't rebuild), not every assembly. It is the **exact** content version this engine uses — never composed with the engine's prerelease suffix — and takes the engine line's major.minor with a patch part that is a content counter (`4.4.0`, `4.4.1`, …) unrelated to the engine's patch, not an engine prerelease label. Content with a stable number published for a beta engine serves the stable engine too, with no new samples release: New Project rewrites the `Stride.*` references to the running engine (a plain `dotnet new` keeps the beta it was stamped for). A prerelease content number (`4.4.1-beta1`) is only for content known not to be ready: a stable engine refuses to name it.
|
||||
|
||||
**Consumers** — the GameStudio bridge (`DotNetNewTemplateBridge`) and the `stride` CLI share one resolver ([`ContentTemplateResolver`](../../sources/engine/Stride.Assets/Templates/ContentTemplateResolver.cs)): the exact `StrideSamplesVersion`, installed from the package sources once if missing (the AssetPacks are only ever obtained this way). Starters and Samples are also pinned dependencies of `Stride.GameStudio` (`[4.4.0]`, so an install fails loudly if the content is not on the feed), which is what installs them with Stride and lets the CLI read the content version of any installed engine. The one exception is a developer's own checkout: a `-devN` engine prefers this checkout's pack of the content, exactly `StrideSamplesVersion` plus its suffix (`4.4.0-beta7-dev4`), when one exists. Everything is an exact version: the resolver never takes a higher one, and the install asks NuGet for exactly that version. From local package feeds (`bin/packages`, nugetdev), a dev engine copies only that dev pack into the NuGet cache, never a local file carrying a published version; an engine without a checkout suffix (CI) copies the plain version, which is how CI tests use the branch's content. The CLI does the same for `Stride.Templates.Games`: it uses exactly the version the engine's `Stride.GameStudio` depends on, as GameStudio uses its own. Content packed for an older engine is upgraded on instantiation (New Project keeps the packed version so the package upgraders run when the project is loaded; `stride new` runs the asset compiler's `upgrade` verb); with plain `dotnet new` the project is upgraded the first time it is opened.
|
||||
|
||||
**A normal build never packs the content** (it just answers GameStudio's dependency query with the committed version). To pack this checkout's samples, e.g. while editing them, set `StridePackContentTemplates=true` (`PrepareSamplesForRelease` and `release-samples.yml` do, and CI builds default to it) — the packs are always worktree-suffixed (`4.4.0-beta7-dev2`) so they can never be mistaken for the published content (`-p:StrideSamplesVersion=` packs another number, which only `release-samples.yml` uses). Turning the flag off is enough to go back to the published content: the next build removes this checkout's dev packs (`CleanContentTemplates`, which also runs by hand). It also warns about a pack in a local feed that carries a published version number, since a restore may take it over the published package:
|
||||
|
||||
```bash
|
||||
dotnet msbuild build/Stride.Samples.build -t:UpgradeSamplesVersion # -p:SampleVersion= to override (e.g. a -beta)
|
||||
dotnet msbuild build/Stride.Samples.build -t:SamplesToReleaseVersion # finalize to the clean version, then commit
|
||||
dotnet build sources/templates/Stride.Templates.Samples -p:StridePackContentTemplates=true # or set it in build/Stride.Local.props
|
||||
dotnet msbuild build/Stride.Samples.build -t:CleanContentTemplates # remove the dev packs now
|
||||
```
|
||||
|
||||
**Template package versions** ([`Stride.Templates.Common.targets`](../../sources/templates/Stride.Templates.Common.targets)):
|
||||
### Preparing and releasing the samples
|
||||
|
||||
- `Stride.Templates.Games` (NewGame + Library) → **engine-versioned** (`$(StrideNuGetVersion)`), always rebuilt and pushed (small).
|
||||
- `Stride.Templates.Samples` + `Stride.Templates.Games.Starters` + `Stride.Templates.AssetPacks` → **content-versioned** at the committed `StrideSamplesVersion` ([`StrideSamplesVersion.props`](../../sources/templates/StrideSamplesVersion.props)); they only change (and re-publish) when the samples are actually bumped. The GameStudio bridge resolves the highest published version `<=` the engine version for these.
|
||||
`StrideSamplesVersion` is never edited by hand. Sample changes are merged like any other change, and the branch keeps naming the content already on the feed, so nothing breaks in between. [`release-samples.yml`](../../.github/workflows/release-samples.yml) publishes them under a new number and only then commits that number. A samples release, start to finish:
|
||||
|
||||
A user instantiating any template gets their installed engine version stamped in (the package upgrader rewrites `Stride.*` references on instantiation), so a lagging template package still produces a project on the user's current Stride.
|
||||
1. **Bring the samples up to the engine** when the engine changed under them: run `PrepareSamplesForRelease` (below), open the samples in GameStudio and fix what the upgraders left, run `SamplesToReleaseEngine`, and merge the result with a normal PR. Its CI packs the content and runs the template, editor and sample tests on it. Plain sample edits skip this step.
|
||||
2. **Dispatch `release-samples.yml`** on the branch (`stride-release-managers` only) with `sign` and `deploy` and the `version` to publish (`X.Y.Z`, or a typed prerelease for content not ready yet). The packs are stamped for the branch's committed engine version (suffix included, e.g. `4.4.0-beta8`), shown in the run summary. An empty `version` takes the committed one's next number (`4.4.0` → `4.4.1`; the older prerelease `4.4.0-beta7` → `4.4.0`), or `<engine line>.0` when the committed content is from an older line (`4.4.2` on a 4.5 branch → `4.5.0`). The major.minor must be the branch's engine line: `4.4.x` content is released from the 4.4 branch only. A newer engine may keep naming older-line content (it is upgraded on New Project) until its own first samples release. The patch part is a counter with no relation to the engine's patch (engine `4.4.0` can use samples `4.4.1`).
|
||||
3. **The workflow** checks the number (valid, after the committed one, not already released), packs the three packages release-shaped (`PackageSamples` in `build/Stride.build`), runs the template smoke tests against the repo build, and signs. With `deploy`, it pushes them, tags `samples/<version>` on the built commit, creates a GitHub Release, and last commits `StrideSamplesVersion` = `<version>` to the branch. Without `deploy`, nothing is published or committed.
|
||||
4. **Release the engine** as usual. Engines built from then on use the new content.
|
||||
|
||||
`PrepareSamplesForRelease` does the mechanical part of step 1: `SamplesToDevEngine`, `UpgradeSamples` (every sample through the **real package upgraders**: migrate assets to the current format, rewrite `Stride.*` to this checkout's dev build, restore from the local dev feed), `SamplesToReleaseEngine`, and a `-devN` pack of the content to try it. It then prints the part that stays manual on purpose.
|
||||
|
||||
```bash
|
||||
dotnet msbuild build/Stride.Samples.build -t:PrepareSamplesForRelease # -p:SamplesEngineVersion= for the engine version the samples reference
|
||||
```
|
||||
|
||||
**Checks at engine release**: an engine naming content that is not on the feed would break every New Project, so `release-deploy.yml` refuses to deploy an engine whose `StrideSamplesVersion` is not published, or that is stable while naming prerelease content. Samples changed since the published content (its `samples/<version>` tag) only produce a warning, with the file list in the run summary: the engine ships with the published content, which is fine for work not released yet, and the release manager decides whether a samples release should come first. A PR that changes the samples gets a warning from the change-detection job ([`changes.yml`](../../.github/workflows/changes.yml)) as a reminder that a samples release is pending; local builds never check this (local content packs are always `-devN`).
|
||||
|
||||
CI builds (`CI`, `GITHUB_ACTIONS` or `TF_BUILD` set, except engine release builds) pack the content by default, so the tests that instantiate templates (`Stride.Templates.Tests`, `tests/editor`, `tests/enduser/Stride.Samples.Tests`, `Stride.Packaging.Tests`) run on this checkout's samples and don't depend on the content being published. Locally they use the published content unless the flag is on. The flag is only ever a normal property (Local.props, command line, CI default), never set on a single `ProjectReference`: that would build the template projects, and everything they reference, a second time with different global properties.
|
||||
|
||||
GameStudio never downloads content while it starts. A missing Starters or Samples package is downloaded in the background and its templates appear in New Project once ready; AssetPacks are downloaded when the New Game dialog first offers them.
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace JumpyJet
|
||||
if (!isRunning)
|
||||
continue;
|
||||
|
||||
var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
// apply impulse on the touch/space
|
||||
if (Input.IsKeyPressed(Keys.Space) || UserTappedScreen())
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace JumpyJet
|
||||
if (!isScrolling)
|
||||
return;
|
||||
|
||||
var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
for (int i = 0; i < pipeSets.Count; i++)
|
||||
{
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace SpaceEscape.Background
|
||||
if (!isScrolling)
|
||||
return;
|
||||
|
||||
var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
// Check if needed to remove the first block
|
||||
var firstBlock = levelBlocks[0];
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace AnimatedModel
|
||||
rotationSpeed = 200f * Input.PointerEvents.Sum(x => x.DeltaPosition.X);
|
||||
|
||||
rotationSpeed *= 0.93f;
|
||||
var elapsedTime = (float) Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var elapsedTime = (float) Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
Entity.Transform.Rotation *= Quaternion.RotationY(rotationSpeed * elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace SpriteStudioDemo
|
||||
if (inputState == InputState.RunLeft || inputState == InputState.RunRight)
|
||||
{
|
||||
// Update Agent's position
|
||||
var dt = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var dt = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
Entity.Transform.Position.X += ((inputState == InputState.RunRight) ? AgentMoveDistance : -AgentMoveDistance) * dt;
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace TouchInputs
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
var currentTime = Game.DrawTime.Total;
|
||||
var currentTime = Game.UpdateTime.Total;
|
||||
|
||||
keyDown = "";
|
||||
keyEvents = "";
|
||||
@@ -248,7 +248,7 @@ namespace TouchInputs
|
||||
{
|
||||
var screenSize = spriteBatch.VirtualResolution.Value;
|
||||
var position = tuple.Item1;
|
||||
var duration = Game.DrawTime.Total - tuple.Item2;
|
||||
var duration = Game.UpdateTime.Total - tuple.Item2;
|
||||
|
||||
var scale = (float)(0.2f * (1f - duration.TotalSeconds / displayPointerDuration.TotalSeconds));
|
||||
var pointerScreenPosition = new Vector2(position.X * screenSize.X, position.Y * screenSize.Y);
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace ParticlesSample
|
||||
while (secondsCountdown > 0f)
|
||||
{
|
||||
await Script.NextFrame();
|
||||
secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
// Remove
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace ParticlesSample
|
||||
rotationSpeed = 200f * Input.PointerEvents.Sum(x => x.DeltaPosition.X);
|
||||
|
||||
rotationSpeed *= 0.93f;
|
||||
var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var elapsedTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
Entity.Transform.Rotation *= Quaternion.RotationY(rotationSpeed * elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ namespace FirstPersonShooter.Player
|
||||
|
||||
// Update current animation
|
||||
var currentTicks = TimeSpan.FromTicks((long)(currentTime * currentClip.Duration.Ticks));
|
||||
var updatedTicks = currentTicks.Ticks + (long)(Game.DrawTime.Elapsed.Ticks * TimeFactor);
|
||||
var updatedTicks = currentTicks.Ticks + (long)(Game.UpdateTime.WarpElapsed.Ticks * TimeFactor);
|
||||
|
||||
var currentClipFinished = (updatedTicks >= currentClip.Duration.Ticks);
|
||||
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ namespace FirstPersonShooter.Player
|
||||
while (secondsCountdown > 0f)
|
||||
{
|
||||
await Script.NextFrame();
|
||||
secondsCountdown -= (float) Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
secondsCountdown -= (float) Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
remainingBullets = 9;
|
||||
@@ -77,7 +77,7 @@ namespace FirstPersonShooter.Player
|
||||
bool didReload;
|
||||
reloadEvent.TryReceive(out didReload);
|
||||
|
||||
cooldownRemaining = (cooldownRemaining > 0) ? (cooldownRemaining - (float)this.Game.UpdateTime.Elapsed.TotalSeconds) : 0f;
|
||||
cooldownRemaining = (cooldownRemaining > 0) ? (cooldownRemaining - (float)this.Game.UpdateTime.WarpElapsed.TotalSeconds) : 0f;
|
||||
if (cooldownRemaining > 0)
|
||||
return; // Can't shoot yet
|
||||
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ namespace FirstPersonShooter
|
||||
while (secondsCountdown > 0f)
|
||||
{
|
||||
await Script.NextFrame();
|
||||
secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
// Remove
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public class CoinRotation : SyncScript
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
animationTimer += (float) Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
animationTimer += (float) Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
if (animationTimer >= animationInterval)
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ public class PlayerController : SyncScript
|
||||
|
||||
private void HandleAnimation(bool isMoving)
|
||||
{
|
||||
animationTimer += (float) Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
animationTimer += (float) Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
if (CharacterComponent.IsGrounded && !isMoving)
|
||||
{
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fb81f0291d24d00db1dbaaa27feffa02112aa2bea36b3ce72d6b5d9590e94488
|
||||
size 697936
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a4baf16263d68608d2b305656b7211d6379db2907aac7f0462997855a056db5
|
||||
size 1220368
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8aeb50db7fe129ada00bc43b0287cf85ad4d50c1948881429d7d78b7ca43b93
|
||||
size 678384
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:733a32e804520e5cb31ef0f2c896c1ff052a40efddd3316f026cd87c65adef64
|
||||
size 707040
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:297263f9e24d27c1751a47a0fc8ebd94d62dca7c44ee60b1aaee9a9fc7e83730
|
||||
size 567584
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8cbf666ae6e21d726de3142bb392ec3bc474264f1b9b98f7b4bb9fd7c349230d
|
||||
size 626160
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3480e7724f490cacfce3595b006c49f7cdb44a8d7a49b83335bdb94d6a35a113
|
||||
size 666704
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c41391150ff639bf50d57e8c303936c02bcddcb3feecbf59961b25bcecb60a2
|
||||
size 835840
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e91084566966add6697b6aeaa00135e00b412d9865efce8e23d8b254cb689c3
|
||||
size 34488508
|
||||
-188
@@ -1,188 +0,0 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
using System;
|
||||
using Stride.Core;
|
||||
using Stride.Core.Mathematics;
|
||||
using Stride.Engine;
|
||||
using Stride.Input;
|
||||
|
||||
namespace ThirdPersonPlatformer
|
||||
{
|
||||
/// <summary>
|
||||
/// A script that allows to move and rotate an entity through keyboard, mouse and touch input to provide basic camera navigation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The entity can be moved using W, A, S, D, Q and E, arrow keys or dragging/scaling using multi-touch.
|
||||
/// Rotation is achieved using the Numpad, the mouse while holding the right mouse button, or dragging using single-touch.
|
||||
/// </remarks>
|
||||
public class BasicCameraController : SyncScript
|
||||
{
|
||||
private const float MaximumPitch = MathUtil.PiOverTwo * 0.99f;
|
||||
|
||||
private Vector3 upVector;
|
||||
private Vector3 translation;
|
||||
private float yaw;
|
||||
private float pitch;
|
||||
|
||||
public Vector3 KeyboardMovementSpeed { get; set; } = new Vector3(5.0f);
|
||||
|
||||
public Vector3 TouchMovementSpeed { get; set; } = new Vector3(40, 40, 20);
|
||||
|
||||
public float SpeedFactor { get; set; } = 5.0f;
|
||||
|
||||
public Vector2 KeyboardRotationSpeed { get; set; } = new Vector2(3.0f);
|
||||
|
||||
public Vector2 MouseRotationSpeed { get; set; } = new Vector2(90.0f, 60.0f);
|
||||
|
||||
public Vector2 TouchRotationSpeed { get; set; } = new Vector2(60.0f, 40.0f);
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
base.Start();
|
||||
|
||||
// Default up-direction
|
||||
upVector = Vector3.UnitY;
|
||||
|
||||
// Configure touch input
|
||||
if (!Platform.IsWindowsDesktop)
|
||||
{
|
||||
Input.Gestures.Add(new GestureConfigDrag());
|
||||
Input.Gestures.Add(new GestureConfigComposite());
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
ProcessInput();
|
||||
UpdateTransform();
|
||||
}
|
||||
|
||||
private void ProcessInput()
|
||||
{
|
||||
translation = Vector3.Zero;
|
||||
yaw = 0;
|
||||
pitch = 0;
|
||||
|
||||
// Move with keyboard
|
||||
if (Input.IsKeyDown(Keys.W) || Input.IsKeyDown(Keys.Up))
|
||||
{
|
||||
translation.Z = -KeyboardMovementSpeed.Z;
|
||||
}
|
||||
else if (Input.IsKeyDown(Keys.S) || Input.IsKeyDown(Keys.Down))
|
||||
{
|
||||
translation.Z = KeyboardMovementSpeed.Z;
|
||||
}
|
||||
|
||||
if (Input.IsKeyDown(Keys.A) || Input.IsKeyDown(Keys.Left))
|
||||
{
|
||||
translation.X = -KeyboardMovementSpeed.X;
|
||||
}
|
||||
else if (Input.IsKeyDown(Keys.D) || Input.IsKeyDown(Keys.Right))
|
||||
{
|
||||
translation.X = KeyboardMovementSpeed.X;
|
||||
}
|
||||
|
||||
if (Input.IsKeyDown(Keys.Q))
|
||||
{
|
||||
translation.Y = -KeyboardMovementSpeed.Y;
|
||||
}
|
||||
else if (Input.IsKeyDown(Keys.E))
|
||||
{
|
||||
translation.Y = KeyboardMovementSpeed.Y;
|
||||
}
|
||||
|
||||
// Alternative translation speed
|
||||
if (Input.IsKeyDown(Keys.LeftShift) || Input.IsKeyDown(Keys.RightShift))
|
||||
{
|
||||
translation *= SpeedFactor;
|
||||
}
|
||||
|
||||
// Rotate with keyboard
|
||||
if (Input.IsKeyDown(Keys.NumPad2))
|
||||
{
|
||||
pitch = KeyboardRotationSpeed.X;
|
||||
}
|
||||
else if (Input.IsKeyDown(Keys.NumPad8))
|
||||
{
|
||||
pitch = -KeyboardRotationSpeed.X;
|
||||
}
|
||||
|
||||
if (Input.IsKeyDown(Keys.NumPad4))
|
||||
{
|
||||
yaw = KeyboardRotationSpeed.Y;
|
||||
}
|
||||
else if (Input.IsKeyDown(Keys.NumPad6))
|
||||
{
|
||||
yaw = -KeyboardRotationSpeed.Y;
|
||||
}
|
||||
|
||||
// Rotate with mouse
|
||||
if (Input.IsMouseButtonDown(MouseButton.Right))
|
||||
{
|
||||
Input.LockMousePosition();
|
||||
Game.IsMouseVisible = false;
|
||||
|
||||
yaw = -Input.MouseDelta.X * MouseRotationSpeed.X;
|
||||
pitch = -Input.MouseDelta.Y * MouseRotationSpeed.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
Input.UnlockMousePosition();
|
||||
Game.IsMouseVisible = true;
|
||||
}
|
||||
|
||||
// Handle gestures
|
||||
foreach (var gestureEvent in Input.GestureEvents)
|
||||
{
|
||||
switch (gestureEvent.Type)
|
||||
{
|
||||
// Rotate by dragging
|
||||
case GestureType.Drag:
|
||||
var drag = (GestureEventDrag)gestureEvent;
|
||||
var dragDistance = drag.DeltaTranslation;
|
||||
yaw = -dragDistance.X * TouchRotationSpeed.X;
|
||||
pitch = -dragDistance.Y * TouchRotationSpeed.Y;
|
||||
break;
|
||||
|
||||
// Move along z-axis by scaling and in xy-plane by multi-touch dragging
|
||||
case GestureType.Composite:
|
||||
var composite = (GestureEventComposite)gestureEvent;
|
||||
translation.X = -composite.DeltaTranslation.X * TouchMovementSpeed.X;
|
||||
translation.Y = -composite.DeltaTranslation.Y * TouchMovementSpeed.Y;
|
||||
translation.Z = -(float)Math.Log(composite.DeltaScale + 1) * TouchMovementSpeed.Z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTransform()
|
||||
{
|
||||
var elapsedTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
|
||||
translation *= elapsedTime;
|
||||
yaw *= elapsedTime;
|
||||
pitch *= elapsedTime;
|
||||
|
||||
// Get the local coordinate system
|
||||
var rotation = Matrix.RotationQuaternion(Entity.Transform.Rotation);
|
||||
|
||||
// Enforce the global up-vector by adjusting the local x-axis
|
||||
var right = Vector3.Cross(rotation.Forward, upVector);
|
||||
var up = Vector3.Cross(right, rotation.Forward);
|
||||
|
||||
// Stabilize
|
||||
right.Normalize();
|
||||
up.Normalize();
|
||||
|
||||
// Adjust pitch. Prevent it from exceeding up and down facing. Stabilize edge cases.
|
||||
var currentPitch = MathUtil.PiOverTwo - (float)Math.Acos(Vector3.Dot(rotation.Forward, upVector));
|
||||
pitch = MathUtil.Clamp(currentPitch + pitch, -MaximumPitch, MaximumPitch) - currentPitch;
|
||||
|
||||
// Move in local coordinates
|
||||
Entity.Transform.Position += Vector3.TransformCoordinate(translation, rotation);
|
||||
|
||||
// Yaw around global up-vector, pitch and roll in local space
|
||||
Entity.Transform.Rotation *= Quaternion.RotationAxis(right, pitch) * Quaternion.RotationAxis(upVector, yaw);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -139,8 +139,7 @@ namespace ThirdPersonPlatformer.Player
|
||||
animationClipWalkLerp2 = AnimationRun;
|
||||
}
|
||||
|
||||
// Use DrawTime rather than UpdateTime
|
||||
var time = Game.DrawTime;
|
||||
var time = Game.UpdateTime;
|
||||
// This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration
|
||||
long blendedMaxDuration = 0;
|
||||
blendedMaxDuration =
|
||||
@@ -150,7 +149,7 @@ namespace ThirdPersonPlatformer.Player
|
||||
|
||||
currentTicks = blendedMaxDuration == 0
|
||||
? TimeSpan.Zero
|
||||
: TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) %
|
||||
: TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) %
|
||||
blendedMaxDuration);
|
||||
|
||||
currentTime = ((double)currentTicks.Ticks / (double)blendedMaxDuration);
|
||||
@@ -160,7 +159,7 @@ namespace ThirdPersonPlatformer.Player
|
||||
{
|
||||
var speedFactor = 1;
|
||||
var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationJumpStart.Duration.Ticks));
|
||||
var updatedTicks = currentTicks.Ticks + (long)(Game.DrawTime.Elapsed.Ticks * TimeFactor * speedFactor);
|
||||
var updatedTicks = currentTicks.Ticks + (long)(Game.UpdateTime.WarpElapsed.Ticks * TimeFactor * speedFactor);
|
||||
|
||||
if (updatedTicks < AnimationJumpStart.Duration.Ticks)
|
||||
{
|
||||
@@ -177,10 +176,9 @@ namespace ThirdPersonPlatformer.Player
|
||||
|
||||
private void UpdateAirborne()
|
||||
{
|
||||
// Use DrawTime rather than UpdateTime
|
||||
var time = Game.DrawTime;
|
||||
var time = Game.UpdateTime;
|
||||
var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationJumpMid.Duration.Ticks));
|
||||
currentTicks = TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) %
|
||||
currentTicks = TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) %
|
||||
AnimationJumpMid.Duration.Ticks);
|
||||
currentTime = ((double)currentTicks.Ticks / (double)AnimationJumpMid.Duration.Ticks);
|
||||
}
|
||||
@@ -189,7 +187,7 @@ namespace ThirdPersonPlatformer.Player
|
||||
{
|
||||
var speedFactor = 1;
|
||||
var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationJumpEnd.Duration.Ticks));
|
||||
var updatedTicks = currentTicks.Ticks + (long) (Game.DrawTime.Elapsed.Ticks * TimeFactor * speedFactor);
|
||||
var updatedTicks = currentTicks.Ticks + (long) (Game.UpdateTime.WarpElapsed.Ticks * TimeFactor * speedFactor);
|
||||
|
||||
if (updatedTicks < AnimationJumpEnd.Duration.Ticks)
|
||||
{
|
||||
|
||||
-1
@@ -61,7 +61,6 @@ namespace ThirdPersonPlatformer.Player
|
||||
/// </summary>
|
||||
public override void Update()
|
||||
{
|
||||
// var dt = Game.UpdateTime.Elapsed.Milliseconds * 0.001;
|
||||
Move(MaxRunSpeed);
|
||||
|
||||
Jump();
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace TopDownRPG.Core
|
||||
while (secondsCountdown > 0f)
|
||||
{
|
||||
await script.Script.NextFrame();
|
||||
secondsCountdown -= (float)script.Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
secondsCountdown -= (float)script.Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
// Remove
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace TopDownRPG.Gameplay
|
||||
|
||||
public void UpdateAnimation()
|
||||
{
|
||||
var dt = (float) Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var dt = (float) Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
Entity.Transform.Rotation *= Quaternion.RotationMatrix(Matrix.RotationY(spinSpeed * (float)(dt * Math.PI)));
|
||||
|
||||
if (!activated)
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace TopDownRPG.Gameplay
|
||||
if (!activated)
|
||||
return;
|
||||
|
||||
var dt = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var dt = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
animationTime += dt * 8;
|
||||
var coinHeight = Math.Max(0, Math.Sin(animationTime));
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace TopDownRPG.Gameplay
|
||||
|
||||
public void UpdateAnimation()
|
||||
{
|
||||
var dt = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var dt = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
if (!activated)
|
||||
return;
|
||||
|
||||
+3
-4
@@ -121,8 +121,7 @@ namespace TopDownRPG.Player
|
||||
animationClipWalkLerp2 = AnimationRun;
|
||||
}
|
||||
|
||||
// Use DrawTime rather than UpdateTime
|
||||
var time = Game.DrawTime;
|
||||
var time = Game.UpdateTime;
|
||||
// This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration
|
||||
long blendedMaxDuration = 0;
|
||||
blendedMaxDuration =
|
||||
@@ -132,7 +131,7 @@ namespace TopDownRPG.Player
|
||||
|
||||
currentTicks = blendedMaxDuration == 0
|
||||
? TimeSpan.Zero
|
||||
: TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) %
|
||||
: TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) %
|
||||
blendedMaxDuration);
|
||||
|
||||
currentTime = ((double)currentTicks.Ticks / (double)blendedMaxDuration);
|
||||
@@ -142,7 +141,7 @@ namespace TopDownRPG.Player
|
||||
{
|
||||
var speedFactor = 1;
|
||||
var currentTicks = TimeSpan.FromTicks((long)(currentTime * AnimationPunch.Duration.Ticks));
|
||||
var updatedTicks = currentTicks.Ticks + (long)(Game.DrawTime.Elapsed.Ticks * TimeFactor * speedFactor);
|
||||
var updatedTicks = currentTicks.Ticks + (long)(Game.UpdateTime.WarpElapsed.Ticks * TimeFactor * speedFactor);
|
||||
|
||||
if (updatedTicks < AnimationPunch.Duration.Ticks)
|
||||
{
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace TopDownRPG.Player
|
||||
|
||||
private void Attack()
|
||||
{
|
||||
var dt = (float) Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var dt = (float) Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
attackCooldown = (attackCooldown > 0) ? attackCooldown - dt : 0f;
|
||||
|
||||
PunchCollision.Enabled = (attackCooldown > 0);
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ namespace CSharpBeginner.Code
|
||||
public override void Update()
|
||||
{
|
||||
/// We can access Delta time through the static 'Game' object.
|
||||
var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
// We update the total time
|
||||
totalTime += deltaTime;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ namespace CSharpBeginner.Code
|
||||
DebugText.Print("Hold the 1 key down to rotate the blue teapot", new Int2(340, 500));
|
||||
if (Input.IsKeyDown(Keys.D1))
|
||||
{
|
||||
var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
BlueTeapot.Transform.Rotation *= Quaternion.RotationY(0.3f * deltaTime);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ namespace CSharpBeginner.Code
|
||||
public override void Update()
|
||||
{
|
||||
// Keep track of elapsed time
|
||||
var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
elapsedTime += deltaTime;
|
||||
|
||||
// In order to make use of the lerp method, we need to calculate the 'interpolation value': a value going from 0 to 1.
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ namespace CSharpBeginner.Code
|
||||
DebugText.Print("Hold the left mouse button down to rotate the blue teapot", new Int2(400, 600));
|
||||
if (Input.IsMouseButtonDown(MouseButton.Left))
|
||||
{
|
||||
var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
BlueTeapot.Transform.Rotation *= Quaternion.RotationY(0.4f * deltaTime);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ namespace CSharpBeginner.Code
|
||||
public override void Update()
|
||||
{
|
||||
// We use a simple timer
|
||||
timer += (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
timer += (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
if (timer > currentTimer)
|
||||
{
|
||||
// If the entities exist, we remove them from the scene
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ namespace CSharpBeginner.Code
|
||||
// Note: Gamepad sticks can be a negative value. For this example we only check if the value is higher than 0
|
||||
if (forward > 0)
|
||||
{
|
||||
var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
BlueTeapot.Transform.Rotation *= Quaternion.RotationY(0.6f * forward * deltaTime);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ namespace CSharpIntermediate.Code
|
||||
return;
|
||||
}
|
||||
|
||||
var deltaTime = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var deltaTime = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
var curPosition = RegularCharacter.Transform.WorldMatrix.TranslationVector;
|
||||
var nextWaypointPosition = waypoints[waypointIndex];
|
||||
var distanceToWaypoint = Vector3.Distance(curPosition, nextWaypointPosition);
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ namespace CSharpIntermediate.Code
|
||||
movement.X -= 1;
|
||||
}
|
||||
|
||||
var delta = (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var delta = (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
Entity.Transform.Position += movement * delta * MoveSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace UIParticles
|
||||
while (secondsCountdown > 0f)
|
||||
{
|
||||
await Script.NextFrame();
|
||||
secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
// Remove
|
||||
@@ -283,7 +283,7 @@ namespace UIParticles
|
||||
|
||||
private void DrawFuse()
|
||||
{
|
||||
fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.Elapsed.TotalSeconds * 0.04f);
|
||||
fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.WarpElapsed.TotalSeconds * 0.04f);
|
||||
|
||||
var gaugeCurrentRegion = lifeBarGaugeImage.Region;
|
||||
gaugeCurrentRegion.Width = Math.Max(1, fusePercentage * gaugeBarRegion.Width);
|
||||
@@ -323,7 +323,7 @@ namespace UIParticles
|
||||
|
||||
private void NewGameState()
|
||||
{
|
||||
fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.Elapsed.TotalSeconds * 0.03f);
|
||||
fusePercentage = Math.Min(1f, fusePercentage - (float)Game.UpdateTime.WarpElapsed.TotalSeconds * 0.03f);
|
||||
DrawFuse();
|
||||
|
||||
if (desiredState == GameState.EndGame || fusePercentage <= 0f)
|
||||
|
||||
@@ -791,54 +791,70 @@ public static class AssetRegistry
|
||||
|
||||
RegisteredAssetAssemblies.Remove(assembly);
|
||||
|
||||
foreach (var typeToRemove in RegisteredDefaultAssetExtension.Keys.Where(type => type.Assembly == assembly).ToList())
|
||||
foreach (var typeToRemove in RegisteredDefaultAssetExtension.Keys.Where(type => type.Assembly == assembly))
|
||||
{
|
||||
RegisteredDefaultAssetExtension.Remove(typeToRemove);
|
||||
}
|
||||
|
||||
foreach (var typeToRemove in AssetTypes.ToList().Where(type => type.Assembly == assembly))
|
||||
AssetTypes.RemoveWhere(type => type.Assembly == assembly);
|
||||
|
||||
RegisteredPackageSessionAnalysisTypes.RemoveWhere(type => type.Assembly == assembly);
|
||||
|
||||
for (var i = RegisteredImportersInternal.Count - 1; i >= 0; i--)
|
||||
{
|
||||
AssetTypes.Remove(typeToRemove);
|
||||
if (RegisteredImportersInternal[i].GetType().Assembly == assembly)
|
||||
RegisteredImportersInternal.RemoveAt(i);
|
||||
}
|
||||
|
||||
foreach (var typeToRemove in RegisteredPackageSessionAnalysisTypes.Where(type => type.Assembly == assembly).ToList())
|
||||
{
|
||||
RegisteredPackageSessionAnalysisTypes.Remove(typeToRemove);
|
||||
}
|
||||
|
||||
foreach (var instance in RegisteredImportersInternal.Where(instance => instance.GetType().Assembly == assembly).ToList())
|
||||
{
|
||||
RegisteredImportersInternal.Remove(instance);
|
||||
}
|
||||
|
||||
foreach (var typeToRemove in RegisteredFormatVersions.Keys.Where(type => type.Assembly == assembly).ToList())
|
||||
foreach (var typeToRemove in RegisteredFormatVersions.Keys.Where(type => type.Assembly == assembly))
|
||||
{
|
||||
RegisteredFormatVersions.Remove(typeToRemove);
|
||||
}
|
||||
|
||||
foreach (var typeToRemove in RegisteredAssetUpgraders.Keys.Where(type => type.Key.Assembly == assembly).ToList())
|
||||
foreach (var typeToRemove in RegisteredAssetUpgraders.Keys.Where(type => type.Key.Assembly == assembly))
|
||||
{
|
||||
RegisteredAssetUpgraders.Remove(typeToRemove);
|
||||
}
|
||||
|
||||
foreach (var extensionToRemove in RegisteredAssetFileExtensions.Where(keyValue => keyValue.Value.Assembly == assembly).Select(keyValue => keyValue.Key).ToList())
|
||||
foreach (var extensionToRemove in RegisteredAssetFileExtensions.Where(keyValue => keyValue.Value.Assembly == assembly).Select(keyValue => keyValue.Key))
|
||||
{
|
||||
RegisteredAssetFileExtensions.Remove(extensionToRemove);
|
||||
}
|
||||
|
||||
foreach (var upgraderToRemove in RegisteredPackageUpgraders.Where(keyValue => keyValue.Value.GetType().Assembly == assembly).Select(keyValue => keyValue.Key).ToList())
|
||||
lock (AlwaysMarkAsRootAssetTypes)
|
||||
{
|
||||
AlwaysMarkAsRootAssetTypes.RemoveWhere(type => type.Assembly == assembly);
|
||||
}
|
||||
|
||||
// An asset type of this assembly can target a content type of another one (an engine type, for example)
|
||||
foreach (var (contentType, assetTypes) in ContentToAssetTypes)
|
||||
{
|
||||
assetTypes.RemoveAll(type => type.Assembly == assembly);
|
||||
if (assetTypes.Count == 0 || contentType.Assembly == assembly)
|
||||
ContentToAssetTypes.Remove(contentType);
|
||||
}
|
||||
|
||||
foreach (var typeToRemove in AssetToContentTypes.Keys.Where(type => type.Assembly == assembly))
|
||||
{
|
||||
AssetToContentTypes.Remove(typeToRemove);
|
||||
}
|
||||
|
||||
foreach (var upgraderToRemove in RegisteredPackageUpgraders.Where(keyValue => keyValue.Value.GetType().Assembly == assembly).Select(keyValue => keyValue.Key))
|
||||
{
|
||||
RegisteredPackageUpgraders.Remove(upgraderToRemove);
|
||||
}
|
||||
|
||||
foreach (var instance in RegisteredSerializerFactories.Where(instance => instance.GetType().Assembly == assembly).ToList())
|
||||
foreach (var pair in RegisteredAssetFactories.Where(pair => pair.Value.GetType().Assembly == assembly))
|
||||
{
|
||||
RegisteredSerializerFactories.Remove(instance);
|
||||
RegisteredAssetFactories.Remove(pair.Key);
|
||||
}
|
||||
|
||||
foreach (var instance in RegisteredDataVisitNodes.Where(instance => instance.GetType().Assembly == assembly).ToList())
|
||||
RegisteredSerializerFactories.RemoveWhere(instance => instance.GetType().Assembly == assembly);
|
||||
|
||||
for (var i = RegisteredDataVisitNodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
RegisteredDataVisitNodes.Remove(instance);
|
||||
if (RegisteredDataVisitNodes[i].GetType().Assembly == assembly)
|
||||
RegisteredDataVisitNodes.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,9 @@ partial class PackageSession
|
||||
/// Cheap scan for projects with a pending package upgrade that may carry source-code migrations.
|
||||
/// Reuses the cached MSBuild project (no restore) and the same upgrade-needed checks as
|
||||
/// <see cref="PreLoadPackageDependencies"/>: a direct <c>PackageReference</c> whose declared version
|
||||
/// is below the upgrader's target. Returns one entry per (project, upgrader); the runner does the
|
||||
/// version-gate and only opens a workspace when source rules actually apply.
|
||||
/// is below the upgrader's target. Returns one entry per (project, upgrader) for the projects whose
|
||||
/// upgrade was confirmed; the runner does the version-gate and only opens a workspace when source
|
||||
/// rules actually apply.
|
||||
/// </summary>
|
||||
private List<PendingCodeUpgrade> DetectPendingCodeUpgrades(ILogger log, PackageLoadParameters loadParameters)
|
||||
{
|
||||
@@ -105,6 +106,8 @@ partial class PackageSession
|
||||
continue;
|
||||
}
|
||||
|
||||
var projectCodeUpgrades = new List<PendingCodeUpgrade>();
|
||||
var pendingPackageUpgrades = new List<PendingPackageUpgrade>();
|
||||
var seen = new HashSet<PackageUpgrader>();
|
||||
foreach (var packageReference in msProject.GetItems("PackageReference"))
|
||||
{
|
||||
@@ -123,8 +126,13 @@ partial class PackageSession
|
||||
if (!seen.Add(upgrader))
|
||||
continue;
|
||||
|
||||
result.Add(new PendingCodeUpgrade(upgrader, project.FullPath, range.MinVersion));
|
||||
projectCodeUpgrades.Add(new PendingCodeUpgrade(upgrader, project.FullPath, range.MinVersion));
|
||||
pendingPackageUpgrades.Add(new PendingPackageUpgrade(upgrader, new PackageDependency(packageReference.EvaluatedInclude, range), null));
|
||||
}
|
||||
|
||||
// The source migration writes user files, so it needs the same confirmation as the package upgrade.
|
||||
if (projectCodeUpgrades.Count > 0 && PackageLoadParameters.ShouldUpgrade(RequestPackageUpgrade(project.Package, pendingPackageUpgrades, loadParameters)))
|
||||
result.AddRange(projectCodeUpgrades);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -275,17 +283,8 @@ partial class PackageSession
|
||||
|
||||
if (pendingPackageUpgrades.Count > 0)
|
||||
{
|
||||
var upgradeAllowed = packageUpgradeAllowed != false ? PackageUpgradeRequestedAnswer.Upgrade : PackageUpgradeRequestedAnswer.DoNotUpgrade;
|
||||
|
||||
// Need upgrades, let's ask user confirmation
|
||||
if (loadParameters.PackageUpgradeRequested != null && !packageUpgradeAllowed.HasValue)
|
||||
{
|
||||
upgradeAllowed = loadParameters.PackageUpgradeRequested(package, pendingPackageUpgrades);
|
||||
if (upgradeAllowed == PackageUpgradeRequestedAnswer.UpgradeAll)
|
||||
packageUpgradeAllowed = true;
|
||||
if (upgradeAllowed == PackageUpgradeRequestedAnswer.DoNotUpgradeAny)
|
||||
packageUpgradeAllowed = false;
|
||||
}
|
||||
var upgradeAllowed = RequestPackageUpgrade(package, pendingPackageUpgrades, loadParameters);
|
||||
|
||||
if (!PackageLoadParameters.ShouldUpgrade(upgradeAllowed))
|
||||
{
|
||||
|
||||
@@ -484,6 +484,7 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder
|
||||
private AssetDependencyManager dependencies;
|
||||
private AssetSourceTracker sourceTracker;
|
||||
private bool? packageUpgradeAllowed;
|
||||
private readonly Dictionary<Package, PackageUpgradeRequestedAnswer> packageUpgradeAnswers = [];
|
||||
public event DirtyFlagChangedDelegate<AssetItem> AssetDirtyChanged;
|
||||
private TaskCompletionSource<int> saveCompletion;
|
||||
|
||||
@@ -996,6 +997,31 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder
|
||||
/// </summary>
|
||||
public void DisarmUpgradeBackup() => UpgradeBackup = null;
|
||||
|
||||
// Asks the front-end at most once per package whether to upgrade it. Every upgrade step (source migration,
|
||||
// project references, assets) goes through here, so nothing is written before the user has answered.
|
||||
private PackageUpgradeRequestedAnswer RequestPackageUpgrade(Package package, IList<PendingPackageUpgrade> pendingPackageUpgrades, PackageLoadParameters loadParameters)
|
||||
{
|
||||
if (packageUpgradeAllowed is { } allowed)
|
||||
return allowed ? PackageUpgradeRequestedAnswer.Upgrade : PackageUpgradeRequestedAnswer.DoNotUpgrade;
|
||||
if (loadParameters.PackageUpgradeRequested is null)
|
||||
return PackageUpgradeRequestedAnswer.Upgrade;
|
||||
if (packageUpgradeAnswers.TryGetValue(package, out var answer))
|
||||
return answer;
|
||||
|
||||
answer = loadParameters.PackageUpgradeRequested(package, pendingPackageUpgrades);
|
||||
packageUpgradeAnswers[package] = answer;
|
||||
if (answer == PackageUpgradeRequestedAnswer.UpgradeAll)
|
||||
packageUpgradeAllowed = true;
|
||||
if (answer == PackageUpgradeRequestedAnswer.DoNotUpgradeAny)
|
||||
packageUpgradeAllowed = false;
|
||||
|
||||
// The callback may have opted out of the backup (e.g. the GameStudio dialog's checkbox).
|
||||
if (!loadParameters.BackupBeforeUpgrade)
|
||||
DisarmUpgradeBackup();
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure packages have their dependencies loaded.
|
||||
/// </summary>
|
||||
@@ -1498,6 +1524,7 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder
|
||||
packagesCopy.Remove(package);
|
||||
|
||||
pendingPackageUpgradesPerPackage.Remove(package);
|
||||
packageUpgradeAnswers.Remove(package);
|
||||
|
||||
IsDirty = true;
|
||||
}
|
||||
@@ -1596,22 +1623,8 @@ public sealed partial class PackageSession : IDisposable, IAssetFinder
|
||||
|
||||
if (pendingPackageUpgrades.Count > 0)
|
||||
{
|
||||
var upgradeAllowed = packageUpgradeAllowed != false ? PackageUpgradeRequestedAnswer.Upgrade : PackageUpgradeRequestedAnswer.DoNotUpgrade;
|
||||
|
||||
// Need upgrades, let's ask user confirmation
|
||||
if (loadParameters.PackageUpgradeRequested is not null && !packageUpgradeAllowed.HasValue)
|
||||
{
|
||||
upgradeAllowed = loadParameters.PackageUpgradeRequested(package, pendingPackageUpgrades);
|
||||
if (upgradeAllowed == PackageUpgradeRequestedAnswer.UpgradeAll)
|
||||
packageUpgradeAllowed = true;
|
||||
if (upgradeAllowed == PackageUpgradeRequestedAnswer.DoNotUpgradeAny)
|
||||
packageUpgradeAllowed = false;
|
||||
|
||||
// The confirmation callback may have opted out of the backup (e.g. the GameStudio dialog's
|
||||
// checkbox); disarm so the upgrade writes that follow this point don't snapshot.
|
||||
if (!loadParameters.BackupBeforeUpgrade)
|
||||
DisarmUpgradeBackup();
|
||||
}
|
||||
var upgradeAllowed = RequestPackageUpgrade(package, pendingPackageUpgrades, loadParameters);
|
||||
|
||||
if (!PackageLoadParameters.ShouldUpgrade(upgradeAllowed))
|
||||
{
|
||||
|
||||
@@ -117,6 +117,22 @@ public class PackageStore
|
||||
return package is null ? null : (UDirectory)store.GetRealPath(package);
|
||||
}
|
||||
|
||||
/// <summary>The local-install directory of an installed package (see <see cref="GetLocalPackages"/>).</summary>
|
||||
public UDirectory GetPackageDirectory(NugetLocalPackage package)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(package);
|
||||
return (UDirectory)store.GetRealPath(package);
|
||||
}
|
||||
|
||||
/// <summary>Every installed copy of <paramref name="packageName"/>, for callers that pick a version themselves.</summary>
|
||||
/// <param name="packageName">The package.</param>
|
||||
/// <param name="localBuildRange">The versions to take from local package sources first, null for all.</param>
|
||||
public IEnumerable<NugetLocalPackage> GetLocalPackages(string packageName, PackageVersionRange? localBuildRange = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(packageName);
|
||||
return store.GetLocalPackages(packageName, localBuildRange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches (if needed) and installs <paramref name="packageName"/> at <paramref name="version"/>
|
||||
/// into the local package store, from the configured NuGet sources. Returns the installed
|
||||
@@ -128,6 +144,19 @@ public class PackageStore
|
||||
return store.InstallPackage(packageName, version, [], progress);
|
||||
}
|
||||
|
||||
/// <summary>Raised as <see cref="InstallPackage"/> downloads, with the bytes downloaded so far (throttled to ~250ms, and once more when a download ends).</summary>
|
||||
public event Action<long>? DownloadProgress
|
||||
{
|
||||
add => store.NugetDownloadProgress += value;
|
||||
remove => store.NugetDownloadProgress -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The size of the downloads the current <see cref="InstallPackage"/> has started, in bytes. The final size once
|
||||
/// the downloads have started, so only meaningful for an install of one package (and its small dependencies).
|
||||
/// </summary>
|
||||
public long StartedDownloadBytes => store.NugetStartedDownloadBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default package manager.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
using Stride.Core.Packages;
|
||||
|
||||
namespace Stride.Core.Assets.Templates;
|
||||
|
||||
/// <summary>A template package being downloaded, or one whose download failed.</summary>
|
||||
/// <param name="PackageId">The package.</param>
|
||||
/// <param name="DownloadedBytes">Bytes downloaded so far.</param>
|
||||
/// <param name="TotalBytes">Size of the download, 0 until it starts.</param>
|
||||
/// <param name="Index">Position of this package in its batch, from 1.</param>
|
||||
/// <param name="Count">Number of packages in the batch.</param>
|
||||
/// <param name="Failed">Whether the download failed (the package is not available).</param>
|
||||
public sealed record TemplateDownload(string PackageId, long DownloadedBytes, long TotalBytes = 0, int Index = 1, int Count = 1, bool Failed = false)
|
||||
{
|
||||
/// <summary>Progress from 0 to 1, or null while the size is not known.</summary>
|
||||
public double? Fraction => TotalBytes > 0 ? Math.Clamp((double)DownloadedBytes / TotalBytes, 0, 1) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The template package downloads in progress, for the windows that list templates. The template sources report here
|
||||
/// (<see cref="Report"/>); windows show <see cref="Current"/> and refresh on <see cref="Changed"/>.
|
||||
/// </summary>
|
||||
public static class TemplateDownloads
|
||||
{
|
||||
private static readonly object ThisLock = new();
|
||||
private static TemplateDownload? current;
|
||||
|
||||
/// <summary>The download in progress (or the last failed one), null when there is none.</summary>
|
||||
public static TemplateDownload? Current
|
||||
{
|
||||
get { lock (ThisLock) return current; }
|
||||
}
|
||||
|
||||
/// <summary>Raised when <see cref="Current"/> changes, on the reporting thread (possibly a background one).</summary>
|
||||
public static event Action? Changed;
|
||||
|
||||
/// <summary>Sets <see cref="Current"/>: a download's progress, its failure, or null once it is done.</summary>
|
||||
public static void Report(TemplateDownload? download)
|
||||
{
|
||||
lock (ThisLock)
|
||||
{
|
||||
if (Equals(current, download))
|
||||
return;
|
||||
current = download;
|
||||
}
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>A short status line for <paramref name="download"/>, e.g. "Downloading Stride.Templates.Samples (2/2): 45.2 / 131.9 MB".</summary>
|
||||
public static string Describe(TemplateDownload download)
|
||||
{
|
||||
if (download.Failed)
|
||||
return $"Could not download {download.PackageId}. Check the network connection, then restart Game Studio.";
|
||||
var package = download.Count > 1 ? $"{download.PackageId} ({download.Index}/{download.Count})" : download.PackageId;
|
||||
return download.DownloadedBytes > 0
|
||||
? $"Downloading {package}: {DownloadSize.Format(download.DownloadedBytes, download.TotalBytes)}"
|
||||
: $"Downloading {package}...";
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,27 @@ public class TemplateManager
|
||||
private static readonly List<ITemplateGenerator> Generators = [];
|
||||
private static readonly PackageCollection ExtraPackages = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a package whose templates <see cref="FindTemplates"/> lists. May be called from any thread (template
|
||||
/// packages downloaded in the background register when ready). Registering a package already registered, which
|
||||
/// gained templates since, only signals the change.
|
||||
/// </summary>
|
||||
public static void RegisterPackage(Package package)
|
||||
{
|
||||
ExtraPackages.Add(package);
|
||||
lock (ThisLock)
|
||||
{
|
||||
if (!ExtraPackages.Contains(package))
|
||||
ExtraPackages.Add(package);
|
||||
}
|
||||
PackagesChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after <see cref="RegisterPackage"/> added templates, on the calling thread (possibly a background one).
|
||||
/// Lets an open template list show packages that arrive late.
|
||||
/// </summary>
|
||||
public static event Action? PackagesChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified factory.
|
||||
/// </summary>
|
||||
@@ -56,7 +72,12 @@ public class TemplateManager
|
||||
/// <returns>A sequence containing all registered template descriptions.</returns>
|
||||
public static IEnumerable<TemplateDescription> FindTemplates(PackageSession? session = null)
|
||||
{
|
||||
var packages = session?.Packages.Concat(ExtraPackages).Distinct(DistinctPackagePathComparer.Default) ?? ExtraPackages;
|
||||
List<Package> extraPackages;
|
||||
lock (ThisLock)
|
||||
{
|
||||
extraPackages = [.. ExtraPackages];
|
||||
}
|
||||
var packages = session?.Packages.Concat(extraPackages).Distinct(DistinctPackagePathComparer.Default) ?? extraPackages;
|
||||
// TODO this will not work if the same package has different versions
|
||||
return [.. packages.SelectMany(package => package.Templates).OrderBy(tpl => tpl.Order).ThenBy(tpl => tpl.Name)];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
namespace Stride.Core.Packages;
|
||||
|
||||
/// <summary>Formats package download sizes the same way wherever they are shown (the stride CLI, Game Studio).</summary>
|
||||
public static class DownloadSize
|
||||
{
|
||||
/// <summary>The bytes downloaded so far, e.g. "45.2 MB". No total: it is not known before the downloads start.</summary>
|
||||
public static string Format(long downloadedBytes) => $"{Megabytes(downloadedBytes)} MB";
|
||||
|
||||
/// <summary>
|
||||
/// "45.2 / 131.9 MB", for a download whose size is known (<paramref name="totalBytes"/> above 0), else as
|
||||
/// <see cref="Format(long)"/>.
|
||||
/// </summary>
|
||||
public static string Format(long downloadedBytes, long totalBytes)
|
||||
=> totalBytes > 0 ? $"{Megabytes(downloadedBytes)} / {Megabytes(totalBytes)} MB" : Format(downloadedBytes);
|
||||
|
||||
private static string Megabytes(long bytes) => $"{bytes / 1048576.0:0.0}";
|
||||
}
|
||||
@@ -2,6 +2,12 @@ namespace Stride.Core.Packages;
|
||||
|
||||
interface INugetDownloadProgress
|
||||
{
|
||||
/// <summary>A package download of <paramref name="length"/> bytes started.</summary>
|
||||
void DownloadStarted(long length);
|
||||
|
||||
/// <summary>Additional bytes were read from an in-flight package download.</summary>
|
||||
void DownloadAdvanced(long bytesRead);
|
||||
|
||||
/// <summary>An in-flight package download reached its end.</summary>
|
||||
void DownloadCompleted();
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ internal sealed class DownloadProgressMessageHandler : DelegatingHandler
|
||||
&& response.Content is { } content
|
||||
&& content.Headers.ContentLength is > 0)
|
||||
{
|
||||
progress.DownloadStarted(content.Headers.ContentLength.Value);
|
||||
response.Content = new ProgressHttpContent(content, content.Headers.ContentLength.Value, progress);
|
||||
}
|
||||
return response;
|
||||
@@ -86,6 +87,7 @@ internal sealed class ProgressHttpContent : HttpContent
|
||||
await stream.WriteAsync(buffer.AsMemory(0, read));
|
||||
progress.DownloadAdvanced(read);
|
||||
}
|
||||
progress.DownloadCompleted();
|
||||
}
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
@@ -107,6 +109,7 @@ internal sealed class ProgressReadStream : Stream
|
||||
private readonly Stream inner;
|
||||
private readonly long length;
|
||||
private readonly INugetDownloadProgress progress;
|
||||
private bool completed;
|
||||
|
||||
public ProgressReadStream(Stream inner, long length, INugetDownloadProgress progress)
|
||||
{
|
||||
@@ -118,17 +121,27 @@ internal sealed class ProgressReadStream : Stream
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var read = inner.Read(buffer, offset, count);
|
||||
if (read > 0)
|
||||
progress.DownloadAdvanced(read);
|
||||
Advance(read, count);
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var read = await inner.ReadAsync(buffer, cancellationToken);
|
||||
Advance(read, buffer.Length);
|
||||
return read;
|
||||
}
|
||||
|
||||
// Reports the bytes read, and the end of the download once (a read of 0 bytes for a non-empty buffer).
|
||||
private void Advance(int read, int requested)
|
||||
{
|
||||
if (read > 0)
|
||||
progress.DownloadAdvanced(read);
|
||||
return read;
|
||||
else if (requested > 0 && !completed)
|
||||
{
|
||||
completed = true;
|
||||
progress.DownloadCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
|
||||
@@ -39,6 +39,7 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
|
||||
// Download byte counter (reset per InstallPackage); fed by DownloadProgressHandlerProvider.
|
||||
private long downloadedBytes;
|
||||
private long startedDownloadBytes;
|
||||
private long lastReportTicks;
|
||||
|
||||
private readonly string? oldRootDirectory;
|
||||
@@ -218,9 +219,15 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
/// </summary>
|
||||
public event Action<int>? NugetRestoreInstalling;
|
||||
|
||||
/// <summary>Event raised as packages download, with the total bytes downloaded so far (throttled to ~1s).</summary>
|
||||
/// <summary>Event raised as packages download, with the total bytes downloaded so far (throttled to ~250ms, and once more when a download ends).</summary>
|
||||
public event Action<long>? NugetDownloadProgress;
|
||||
|
||||
/// <summary>
|
||||
/// The size of the downloads the current <see cref="InstallPackage"/> has started so far, in bytes. It grows as
|
||||
/// downloads start, so it is the final size only when a single package is being downloaded.
|
||||
/// </summary>
|
||||
public long NugetStartedDownloadBytes => Interlocked.Read(ref startedDownloadBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Event executed before a package's packageinstall.exe setup step runs.
|
||||
/// </summary>
|
||||
@@ -272,16 +279,20 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
/// <returns>The list of packages sorted from the most recent to the oldest.</returns>
|
||||
public IList<NugetLocalPackage> GetPackagesInstalled(IEnumerable<string> packageIds)
|
||||
{
|
||||
return [.. packageIds.SelectMany(GetLocalPackages).OrderByDescending(p => p.Version)];
|
||||
return [.. packageIds.SelectMany(packageId => GetLocalPackages(packageId)).OrderByDescending(p => p.Version)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of all installed packages.
|
||||
/// </summary>
|
||||
/// <returns>A list of packages.</returns>
|
||||
public IEnumerable<NugetLocalPackage> GetLocalPackages(string packageId)
|
||||
/// <param name="packageId">The package.</param>
|
||||
/// <param name="localBuildRange">
|
||||
/// The versions to take from local-folder sources first (see <see cref="SyncLocalFolderSources"/>), null for all.
|
||||
/// </param>
|
||||
public IEnumerable<NugetLocalPackage> GetLocalPackages(string packageId, PackageVersionRange? localBuildRange = null)
|
||||
{
|
||||
SyncLocalFolderSources(packageId);
|
||||
SyncLocalFolderSources(packageId, localBuildRange);
|
||||
|
||||
var res = new List<NugetLocalPackage>();
|
||||
|
||||
@@ -357,21 +368,26 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors any .nupkg of <paramref name="packageId"/> from configured local-folder NuGet sources
|
||||
/// Mirrors the .nupkg files of <paramref name="packageId"/> from configured local-folder NuGet sources
|
||||
/// (e.g. a worktree's <c>bin/packages</c>) into the global packages folder when the source nupkg
|
||||
/// is newer than the extracted form. Lets dev-built packages flow into <see cref="GetLocalPackages"/>
|
||||
/// without a separate restore step.
|
||||
/// </summary>
|
||||
private void SyncLocalFolderSources(string packageId)
|
||||
/// <param name="versionRange">
|
||||
/// The versions to mirror, null for all: only what the caller's query could use. A lookup for one version does
|
||||
/// not mirror a local copy of another (same id, maybe different content) over the installed one.
|
||||
/// </param>
|
||||
private void SyncLocalFolderSources(string packageId, PackageVersionRange? versionRange = null)
|
||||
{
|
||||
if (InstallPath == null)
|
||||
return;
|
||||
|
||||
lock (localFolderSyncLock)
|
||||
{
|
||||
// Each id is synced at most once per process
|
||||
if (!syncedPackageIds.Add(packageId))
|
||||
// Each id, for each range asked of it, is synced at most once per process.
|
||||
if (!syncedPackageIds.Add($"{packageId}/{versionRange}"))
|
||||
return;
|
||||
var wanted = versionRange?.ToVersionRange();
|
||||
|
||||
if (localFolderResources == null)
|
||||
{
|
||||
@@ -400,19 +416,22 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
|
||||
foreach (var (source, findPackageById) in localFolderResources)
|
||||
{
|
||||
List<NuGetVersion> versions;
|
||||
List<NuGetVersion> sourceVersions;
|
||||
try
|
||||
{
|
||||
// Versions come from file names; no nupkg is opened at this point
|
||||
versions = (await findPackageById.GetAllVersionsAsync(packageId, localFolderCacheContext, NativeLogger, CancellationToken.None).ConfigureAwait(false)).ToList();
|
||||
sourceVersions = (await findPackageById.GetAllVersionsAsync(packageId, localFolderCacheContext, NativeLogger, CancellationToken.None).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var version in versions)
|
||||
foreach (var version in sourceVersions)
|
||||
{
|
||||
// Satisfies, not a float-aware check: the content packs are prereleases (4.4.0-beta7-dev4).
|
||||
if (wanted is not null && !wanted.Satisfies(version))
|
||||
continue;
|
||||
var identity = new PackageIdentity(packageId, version);
|
||||
|
||||
// Try the conventional file name first to avoid opening the package
|
||||
@@ -494,6 +513,7 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
{
|
||||
currentProgressReport = progress;
|
||||
Interlocked.Exchange(ref downloadedBytes, 0);
|
||||
Interlocked.Exchange(ref startedDownloadBytes, 0);
|
||||
Interlocked.Exchange(ref lastReportTicks, 0);
|
||||
try
|
||||
{
|
||||
@@ -556,7 +576,8 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
[
|
||||
new()
|
||||
{
|
||||
LibraryRange = new LibraryRange(packageId, new VersionRange(version.ToNuGetVersion()), LibraryDependencyTarget.Package),
|
||||
// Exactly this version: a bare version is ">=" to NuGet, which would silently take a higher one.
|
||||
LibraryRange = new LibraryRange(packageId, new VersionRange(version.ToNuGetVersion(), true, version.ToNuGetVersion(), true), LibraryDependencyTarget.Package),
|
||||
}
|
||||
],
|
||||
});
|
||||
@@ -659,7 +680,7 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
/// </summary>
|
||||
/// <remarks>It is safe to call it concurrently be cause we operations are done using the FileLock.</remarks>
|
||||
/// <param name="package">Package to uninstall.</param>
|
||||
public async Task UninstallPackage(NugetPackage package, ProgressReport progress)
|
||||
public async Task UninstallPackage(NugetPackage package, ProgressReport? progress)
|
||||
{
|
||||
#if DEBUG
|
||||
var installedPackages = GetPackagesInstalled([package.Id]);
|
||||
@@ -786,7 +807,8 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
return GetPackagesInstalled([packageId]).FirstOrDefault(p => allowPrereleaseVersions || string.IsNullOrEmpty(p.Version.SpecialVersion));
|
||||
}
|
||||
|
||||
var packages = GetLocalPackages(packageId);
|
||||
// The range is the mirror filter too: local-folder sources give up only what this lookup could use.
|
||||
var packages = GetLocalPackages(packageId, constraintProvider is null ? versionRange : null);
|
||||
|
||||
if (!allowUnlisted)
|
||||
{
|
||||
@@ -1024,17 +1046,29 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
return package.Version.SpecialVersion?.StartsWith("dev", StringComparison.Ordinal) == true && !package.Version.SpecialVersion.Contains('.');
|
||||
}
|
||||
|
||||
void INugetDownloadProgress.DownloadStarted(long length)
|
||||
{
|
||||
Interlocked.Add(ref startedDownloadBytes, length);
|
||||
}
|
||||
|
||||
void INugetDownloadProgress.DownloadAdvanced(long bytesRead)
|
||||
{
|
||||
Interlocked.Add(ref downloadedBytes, bytesRead);
|
||||
// Throttle UI updates to ~1s; chunk reads fire far more often than that.
|
||||
// Throttle UI updates to ~250ms; chunk reads fire far more often than that.
|
||||
var now = Environment.TickCount64;
|
||||
if (now - Interlocked.Read(ref lastReportTicks) < 1000)
|
||||
if (now - Interlocked.Read(ref lastReportTicks) < 250)
|
||||
return;
|
||||
Interlocked.Exchange(ref lastReportTicks, now);
|
||||
NugetDownloadProgress?.Invoke(Interlocked.Read(ref downloadedBytes));
|
||||
}
|
||||
|
||||
void INugetDownloadProgress.DownloadCompleted()
|
||||
{
|
||||
// Not throttled: the final count stays shown while the package is extracted.
|
||||
Interlocked.Exchange(ref lastReportTicks, Environment.TickCount64);
|
||||
NugetDownloadProgress?.Invoke(Interlocked.Read(ref downloadedBytes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a package's cached download (<c>nupkg_{id}.{version}.dat</c>) from NuGet's HTTP cache. NuGet keeps
|
||||
/// these indefinitely with no size limit, so removing them on uninstall reclaims space (Stride packages are
|
||||
@@ -1056,7 +1090,7 @@ public partial class NugetStore : INugetDownloadProgress
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
private static void RunPackageInstall(string packageInstall, string arguments, ProgressReport progress)
|
||||
private static void RunPackageInstall(string packageInstall, string arguments, ProgressReport? progress)
|
||||
{
|
||||
// Run packageinstall.exe
|
||||
using var process = Process.Start(new ProcessStartInfo(packageInstall, arguments)
|
||||
|
||||
@@ -75,6 +75,117 @@ public class TestPriorityLinkedQueue
|
||||
CheckPriorityQueue(priorityQueue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The node filling the hole is bigger than its new children, so it has to move down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TestRemovalMovingNodeDown()
|
||||
{
|
||||
var queue = new PriorityNodeQueue<int>();
|
||||
var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]);
|
||||
|
||||
queue.Remove(nodes[1]);
|
||||
|
||||
Assert.Equal(-1, nodes[1].Index);
|
||||
CheckPriorityQueue(queue, [0, 2, 3, 4, 5, 6, 7]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The node filling the hole is smaller than its new parent, so it has to move up.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TestRemovalMovingNodeUp()
|
||||
{
|
||||
var queue = new PriorityNodeQueue<int>();
|
||||
var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]);
|
||||
|
||||
queue.Remove(nodes[4]);
|
||||
|
||||
Assert.Equal(-1, nodes[4].Index);
|
||||
CheckPriorityQueue(queue, [0, 1, 2, 3, 4, 6, 7]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The node sitting last leaves no hole behind, so nothing moves.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TestRemovalOfTheLastNode()
|
||||
{
|
||||
var queue = new PriorityNodeQueue<int>();
|
||||
var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]);
|
||||
|
||||
queue.Remove(nodes[7]);
|
||||
|
||||
Assert.Equal(-1, nodes[7].Index);
|
||||
CheckPriorityQueue(queue, [0, 1, 2, 4, 5, 6, 7]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRemovalWithDescendingComparer()
|
||||
{
|
||||
var queue = new PriorityNodeQueue<int>(Comparer<int>.Create((a, b) => b.CompareTo(a)));
|
||||
var nodes = Enqueue(queue, [0, 1, 4, 2, 5, 6, 7, 3]);
|
||||
|
||||
queue.Remove(nodes[4]);
|
||||
|
||||
CheckPriorityQueue(queue, [7, 6, 4, 3, 2, 1, 0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRandomRemoval()
|
||||
{
|
||||
var random = new Random(0);
|
||||
var queue = new PriorityNodeQueue<int>();
|
||||
List<PriorityQueueNode<int>> nodes = [];
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
nodes.Add(queue.Enqueue(random.Next(100)));
|
||||
}
|
||||
|
||||
var expected = nodes.Select(x => x.Value).ToList();
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
var node = nodes[random.Next(nodes.Count)];
|
||||
queue.Remove(node);
|
||||
nodes.Remove(node);
|
||||
expected.Remove(node.Value);
|
||||
|
||||
Assert.Equal(-1, node.Index);
|
||||
Assert.Equal(expected.Count, queue.Count);
|
||||
}
|
||||
|
||||
expected.Sort();
|
||||
|
||||
CheckPriorityQueue(queue, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestClearReleasesNodes()
|
||||
{
|
||||
var queue = new PriorityNodeQueue<int>();
|
||||
var node = queue.Enqueue(5);
|
||||
|
||||
queue.Clear();
|
||||
|
||||
Assert.Equal(-1, node.Index);
|
||||
|
||||
// A released node belongs to no queue anymore, so it can be queued again
|
||||
queue.Enqueue(node);
|
||||
|
||||
Assert.Equal(5, queue.Peek());
|
||||
}
|
||||
|
||||
private static List<PriorityQueueNode<int>> Enqueue(PriorityNodeQueue<int> priorityQueue, int[] values)
|
||||
{
|
||||
List<PriorityQueueNode<int>> nodes = [];
|
||||
foreach (var value in values)
|
||||
{
|
||||
nodes.Add(priorityQueue.Enqueue(value));
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static void CheckPriorityQueue(PriorityNodeQueue<int> priorityQueue)
|
||||
{
|
||||
int lastItem = int.MinValue;
|
||||
@@ -85,4 +196,16 @@ public class TestPriorityLinkedQueue
|
||||
lastItem = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains the queue, which must return every expected item, in the order given.
|
||||
/// </summary>
|
||||
private static void CheckPriorityQueue(PriorityNodeQueue<int> priorityQueue, IEnumerable<int> expectedItems)
|
||||
{
|
||||
var items = new List<int>();
|
||||
while (!priorityQueue.Empty)
|
||||
items.Add(priorityQueue.Dequeue());
|
||||
|
||||
Assert.Equal(expectedItems, items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,90 @@ public class TestPriorityQueue
|
||||
CheckPriorityQueue(priorityQueue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The item filling the hole is bigger than its new children, so it has to move down.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TestRemovalMovingItemDown()
|
||||
{
|
||||
var queue = new PriorityQueue<int>();
|
||||
foreach (var value in new[] { 0, 1, 4, 2, 5, 6, 7, 3 })
|
||||
queue.Enqueue(value);
|
||||
|
||||
queue.Remove(1);
|
||||
|
||||
CheckPriorityQueue(queue, [0, 2, 3, 4, 5, 6, 7]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The item filling the hole is smaller than its new parent, so it has to move up.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TestRemovalMovingItemUp()
|
||||
{
|
||||
var queue = new PriorityQueue<int>();
|
||||
foreach (var value in new[] { 26, 38, 33, 54, 69, 64, 21 })
|
||||
queue.Enqueue(value);
|
||||
|
||||
queue.Remove(54);
|
||||
|
||||
CheckPriorityQueue(queue, [21, 26, 33, 38, 64, 69]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The item sitting last leaves no hole behind, so nothing moves.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TestRemovalOfTheLastItem()
|
||||
{
|
||||
var queue = new PriorityQueue<int>();
|
||||
foreach (var value in new[] { 0, 1, 4, 2, 5, 6, 7, 3 })
|
||||
queue.Enqueue(value);
|
||||
|
||||
queue.Remove(3);
|
||||
|
||||
CheckPriorityQueue(queue, [0, 1, 2, 4, 5, 6, 7]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRemovalWithDescendingComparer()
|
||||
{
|
||||
var queue = new PriorityQueue<int>(Comparer<int>.Create((a, b) => b.CompareTo(a)));
|
||||
foreach (var value in new[] { 0, 1, 4, 2, 5, 6, 7, 3 })
|
||||
queue.Enqueue(value);
|
||||
|
||||
queue.Remove(5);
|
||||
|
||||
CheckPriorityQueue(queue, [7, 6, 4, 3, 2, 1, 0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestRandomRemoval()
|
||||
{
|
||||
var random = new Random(0);
|
||||
var queue = new PriorityQueue<int>();
|
||||
var expected = new List<int>();
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
var value = random.Next(100);
|
||||
queue.Enqueue(value);
|
||||
expected.Add(value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
var value = expected[random.Next(expected.Count)];
|
||||
queue.Remove(value);
|
||||
expected.Remove(value);
|
||||
|
||||
Assert.Equal(expected.Count, queue.Count);
|
||||
}
|
||||
|
||||
expected.Sort();
|
||||
|
||||
CheckPriorityQueue(queue, expected);
|
||||
}
|
||||
|
||||
private static void CheckPriorityQueue(PriorityQueue<int> priorityQueue)
|
||||
{
|
||||
int lastItem = int.MinValue;
|
||||
@@ -85,4 +169,16 @@ public class TestPriorityQueue
|
||||
lastItem = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains the queue, which must return every expected item, in the order given.
|
||||
/// </summary>
|
||||
private static void CheckPriorityQueue(PriorityQueue<int> priorityQueue, IEnumerable<int> expectedItems)
|
||||
{
|
||||
var items = new List<int>();
|
||||
while (!priorityQueue.Empty)
|
||||
items.Add(priorityQueue.Dequeue());
|
||||
|
||||
Assert.Equal(expectedItems, items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@ namespace Stride.Core.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a priority queue of type T.
|
||||
///
|
||||
///
|
||||
/// Elements may be added to the queue in any order, but when we pull
|
||||
/// elements out of the queue, they will be returned in 'ascending' order.
|
||||
/// Adding new elements into the queue may be done at any time, so this is
|
||||
/// useful to implement a dynamically growing and shrinking queue. Both adding
|
||||
/// an element and removing the first element are log(N) operations.
|
||||
///
|
||||
/// The queue is implemented using a priority-heap data structure. For more
|
||||
/// an element and removing the first element are log(N) operations.
|
||||
///
|
||||
/// The queue is implemented using a priority-heap data structure. For more
|
||||
/// details on this elegant and simple data structure see "Programming Pearls"
|
||||
/// in our library. The tree is implemented atop a list, where 2N and 2N+1 are
|
||||
/// in our library. The tree is implemented atop a list, where 2N+1 and 2N+2 are
|
||||
/// the child nodes of node N. The tree is balanced and left-aligned so there
|
||||
/// are no 'holes' in this list.
|
||||
/// </summary>
|
||||
@@ -40,6 +40,9 @@ public class PriorityNodeQueue<T>
|
||||
/// <summary>Clear all the elements from the priority queue</summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var item in items)
|
||||
item.Index = -1;
|
||||
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
@@ -53,48 +56,21 @@ public class PriorityNodeQueue<T>
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// The element to return is of course the first element in the array,
|
||||
// or the root of the tree. However, this will leave a 'hole' there. We
|
||||
// fill up this hole with the last element from the array. This will
|
||||
// break the heap property. So we bubble the element downwards by swapping
|
||||
// it with it's lower child until it reaches it's correct level. The lower
|
||||
// child (one of the orignal elements with index 1 or 2) will now be at the
|
||||
// head of the queue (root of the tree).
|
||||
var nMax = items.Count - 1;
|
||||
var itemMax = items[nMax];
|
||||
itemMax.Index = index;
|
||||
var itemToRemove = items[index];
|
||||
itemToRemove.Index = -1;
|
||||
items[index] = itemMax;
|
||||
items.RemoveAt(nMax); // Move the last element to the top
|
||||
|
||||
var p = index;
|
||||
while (true)
|
||||
{
|
||||
// c is the child we want to swap with. If there
|
||||
// is no child at all, then the heap is balanced
|
||||
var c = p * 2;
|
||||
if (c >= nMax) break;
|
||||
|
||||
// If the second child is smaller than the first, that's the one
|
||||
// we want to swap with this parent.
|
||||
if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) c++;
|
||||
// If the parent is already smaller than this smaller child, then
|
||||
// we are done
|
||||
if (comparer.Compare(items[p].Value, items[c].Value) <= 0)
|
||||
break;
|
||||
|
||||
// Othewise, swap parent and child, and follow down the parent
|
||||
var tmp = items[p];
|
||||
var tmp2 = items[c];
|
||||
tmp.Index = c;
|
||||
tmp2.Index = p;
|
||||
items[p] = tmp2;
|
||||
items[c] = tmp;
|
||||
p = c;
|
||||
}
|
||||
|
||||
// The item leaves a 'hole', which the last item of the list fills up
|
||||
var lastIndex = items.Count - 1;
|
||||
var lastItem = items[lastIndex];
|
||||
items.RemoveAt(lastIndex);
|
||||
item.Index = -1;
|
||||
|
||||
if (index == lastIndex)
|
||||
return;
|
||||
|
||||
items[index] = lastItem;
|
||||
lastItem.Index = index;
|
||||
|
||||
// The item taken from the end can be smaller than its new parent, or bigger than its children
|
||||
if (!MoveUp(index))
|
||||
MoveDown(index);
|
||||
}
|
||||
|
||||
/// <summary>Add an element to the priority queue - O(log(n)) time operation.</summary>
|
||||
@@ -114,35 +90,11 @@ public class PriorityNodeQueue<T>
|
||||
if (item.Index != -1)
|
||||
throw new InvalidOperationException("Item belongs to another PriorityNodeQueue.");
|
||||
|
||||
// We add the item to the end of the list (at the bottom of the
|
||||
// tree). Then, the heap-property could be violated between this element
|
||||
// and it's parent. If this is the case, we swap this element with the
|
||||
// parent (a safe operation to do since the element is known to be less
|
||||
// than it's parent). Now the element move one level up the tree. We repeat
|
||||
// this test with the element and it's new parent. The element, if lesser
|
||||
// than everybody else in the tree will eventually bubble all the way up
|
||||
// to the root of the tree (or the head of the list). It is easy to see
|
||||
// this will take log(N) time, since we are working with a balanced binary
|
||||
// tree.
|
||||
var n = items.Count;
|
||||
// The item goes at the bottom of the tree, then moves up to its level
|
||||
item.Index = items.Count;
|
||||
items.Add(item);
|
||||
item.Index = n;
|
||||
while (n != 0)
|
||||
{
|
||||
var p = n / 2; // This is the 'parent' of this item
|
||||
if (comparer.Compare(items[n].Value, items[p].Value) >= 0)
|
||||
break; // Item >= parent
|
||||
|
||||
// Swap item and parent
|
||||
var tmp = items[n];
|
||||
var tmp2 = items[p];
|
||||
tmp2.Index = n;
|
||||
tmp.Index = p;
|
||||
items[n] = tmp2;
|
||||
items[p] = tmp;
|
||||
|
||||
n = p; // And continue
|
||||
}
|
||||
MoveUp(item.Index);
|
||||
}
|
||||
|
||||
/// <summary>Returns the number of elements in the queue.</summary>
|
||||
@@ -164,49 +116,66 @@ public class PriorityNodeQueue<T>
|
||||
/// <returns>The first element in the queue, in ascending order.</returns>
|
||||
public T Dequeue()
|
||||
{
|
||||
// The element to return is of course the first element in the array,
|
||||
// or the root of the tree. However, this will leave a 'hole' there. We
|
||||
// fill up this hole with the last element from the array. This will
|
||||
// break the heap property. So we bubble the element downwards by swapping
|
||||
// it with it's lower child until it reaches it's correct level. The lower
|
||||
// child (one of the orignal elements with index 1 or 2) will now be at the
|
||||
// head of the queue (root of the tree).
|
||||
var nMax = items.Count - 1;
|
||||
var itemToRemove = items[0];
|
||||
var itemMax = items[nMax];
|
||||
itemMax.Index = 0;
|
||||
var val = itemToRemove.Value;
|
||||
itemToRemove.Index = -1;
|
||||
items[0] = itemMax;
|
||||
items.RemoveAt(nMax); // Move the last element to the top
|
||||
var item = items[0];
|
||||
var value = item.Value;
|
||||
|
||||
var p = 0;
|
||||
while (true)
|
||||
Remove(item);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves an item towards the head of the queue, while it is smaller than its parent.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item to move.</param>
|
||||
/// <returns><c>true</c> if the item moved; otherwise, <c>false</c>.</returns>
|
||||
private bool MoveUp(int index)
|
||||
{
|
||||
var moved = false;
|
||||
while (index > 0)
|
||||
{
|
||||
// c is the child we want to swap with. If there
|
||||
// is no child at all, then the heap is balanced
|
||||
var c = p * 2;
|
||||
if (c >= nMax) break;
|
||||
|
||||
// If the second child is smaller than the first, that's the one
|
||||
// we want to swap with this parent.
|
||||
if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) c++;
|
||||
// If the parent is already smaller than this smaller child, then
|
||||
// we are done
|
||||
if (comparer.Compare(items[p].Value, items[c].Value) <= 0)
|
||||
var parent = (index - 1) / 2;
|
||||
if (comparer.Compare(items[index].Value, items[parent].Value) >= 0)
|
||||
break;
|
||||
|
||||
// Othewise, swap parent and child, and follow down the parent
|
||||
var tmp = items[p];
|
||||
var tmp2 = items[c];
|
||||
tmp.Index = c;
|
||||
tmp2.Index = p;
|
||||
items[p] = tmp2;
|
||||
items[c] = tmp;
|
||||
|
||||
p = c;
|
||||
Swap(index, parent);
|
||||
index = parent;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
return val;
|
||||
return moved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves an item towards the tail of the queue, while it is bigger than one of its children.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the item to move.</param>
|
||||
private void MoveDown(int index)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Stop when the item has no child left
|
||||
var child = (index * 2) + 1;
|
||||
if (child >= items.Count)
|
||||
break;
|
||||
|
||||
// Of the two children, the smaller one takes the place of the parent
|
||||
var rightChild = child + 1;
|
||||
if (rightChild < items.Count && comparer.Compare(items[rightChild].Value, items[child].Value) < 0)
|
||||
child = rightChild;
|
||||
|
||||
if (comparer.Compare(items[index].Value, items[child].Value) <= 0)
|
||||
break;
|
||||
|
||||
Swap(index, child);
|
||||
index = child;
|
||||
}
|
||||
}
|
||||
|
||||
private void Swap(int first, int second)
|
||||
{
|
||||
(items[first], items[second]) = (items[second], items[first]);
|
||||
items[first].Index = first;
|
||||
items[second].Index = second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,39 +38,53 @@ public class PriorityQueue<T>
|
||||
/// <inheritdoc/>
|
||||
public void Remove(T item)
|
||||
{
|
||||
// Find index of item to remove.
|
||||
var index = items.IndexOf(item);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove requested element and place last one instead
|
||||
var maxCount = items.Count - 1;
|
||||
items[index] = items[maxCount];
|
||||
items.RemoveAt(maxCount);
|
||||
|
||||
// Bubble elements so that order is respected again
|
||||
var parentIndex = index;
|
||||
while (true)
|
||||
if (index == maxCount)
|
||||
return;
|
||||
|
||||
// Check if we need to bubble up or down
|
||||
var parent = (index - 1) / 2;
|
||||
if (index > 0 && comparer.Compare(items[index], items[parent]) < 0)
|
||||
{
|
||||
var childIndex = parentIndex * 2;
|
||||
var child = index;
|
||||
while (child > 0)
|
||||
{
|
||||
parent = (child - 1) / 2;
|
||||
if (comparer.Compare(items[child], items[parent]) >= 0)
|
||||
break;
|
||||
|
||||
// Check if there is any child, otherwise we're done
|
||||
if (childIndex >= maxCount)
|
||||
break;
|
||||
(items[parent], items[child]) = (items[child], items[parent]);
|
||||
child = parent;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentIndex = index;
|
||||
while (true)
|
||||
{
|
||||
var left = parentIndex * 2 + 1;
|
||||
if (left >= items.Count)
|
||||
break;
|
||||
|
||||
// Check which of the two child we need to swap
|
||||
if (childIndex + 1 < maxCount && comparer.Compare(items[childIndex + 1], items[childIndex]) < 0)
|
||||
childIndex++;
|
||||
var right = left + 1;
|
||||
var childIndex = left;
|
||||
|
||||
// Order might already be OK, if yes we're done
|
||||
if (comparer.Compare(items[parentIndex], items[childIndex]) <= 0)
|
||||
break;
|
||||
if (right < items.Count && comparer.Compare(items[right], items[left]) < 0)
|
||||
childIndex = right;
|
||||
|
||||
// Need swap
|
||||
(items[parentIndex], items[childIndex]) = (items[childIndex], items[parentIndex]);
|
||||
if (comparer.Compare(items[parentIndex], items[childIndex]) <= 0)
|
||||
break;
|
||||
|
||||
// Continue with child
|
||||
parentIndex = childIndex;
|
||||
(items[parentIndex], items[childIndex]) = (items[childIndex], items[parentIndex]);
|
||||
parentIndex = childIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +101,7 @@ public class PriorityQueue<T>
|
||||
// Bubble elements so that order is respected again
|
||||
while (childIndex != 0)
|
||||
{
|
||||
var parentIndex = childIndex / 2;
|
||||
var parentIndex = (childIndex - 1) / 2;
|
||||
|
||||
// Should we swap with parent?
|
||||
if (comparer.Compare(items[childIndex], items[parentIndex]) >= 0)
|
||||
@@ -133,7 +147,7 @@ public class PriorityQueue<T>
|
||||
while (true)
|
||||
{
|
||||
// Check if there is any child, otherwise we're done
|
||||
var childIndex = parentIndex * 2;
|
||||
var childIndex = (parentIndex * 2) + 1;
|
||||
if (childIndex >= maxCount)
|
||||
break;
|
||||
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ namespace Stride.Assets.Presentation.AssemblyReloading
|
||||
{
|
||||
// If first time, load assembly
|
||||
if (firstTime)
|
||||
loadedAssembly.NewAssembly = assemblyContainer.LoadAssemblyFromPath(loadedAssembly.PackageLoadedAssembly.Path);
|
||||
loadedAssembly.NewAssembly = assemblyContainer.LoadAssemblyFromPath(loadedAssembly.PackageLoadedAssembly.Path, log);
|
||||
|
||||
// Load assembly
|
||||
assembly = newVersion
|
||||
|
||||
@@ -69,8 +69,9 @@
|
||||
<!-- Force Stride.Templates.Games (NewGame, the bundled-with-installer set) to build before
|
||||
GameStudio links. Without this, a fresh checkout that builds only the editor never
|
||||
produces the .nupkg the DotNetNewTemplateBridge resolves via PackageStore at runtime.
|
||||
Sibling packages (Stride.Templates.Games.Starters, Stride.Templates.Samples) build
|
||||
standalone and reach users via the future editor template store / CLI dotnet new install.
|
||||
The content packages (Stride.Templates.Games.Starters, .Samples, .AssetPacks) are not
|
||||
packed by a normal build: the bridge resolves the published content version the engine
|
||||
names (sources/templates/StrideSamplesVersion.props), or this checkout's opted-in dev pack of it.
|
||||
The templates csproj uses TreatAsLocalProperty="StrideSkipAutoPack" so the auto-pack
|
||||
still runs even when an outer build context sets the property — no strip needed here. -->
|
||||
<ProjectReference Include="..\..\templates\Stride.Templates.Games\Stride.Templates.Games.csproj">
|
||||
|
||||
+2
-3
@@ -58,8 +58,7 @@ public class ##Scriptname## : SyncScript, IBlendTreeBuilder
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
// Use DrawTime rather than UpdateTime
|
||||
var time = Game.DrawTime;
|
||||
var time = Game.UpdateTime;
|
||||
|
||||
// This update function will account for animation with different durations, keeping a current time relative to the blended maximum duration
|
||||
long blendedMaxDuration = 0;
|
||||
@@ -67,7 +66,7 @@ public class ##Scriptname## : SyncScript, IBlendTreeBuilder
|
||||
|
||||
var currentTicks = TimeSpan.FromTicks((long)(currentTime * blendedMaxDuration));
|
||||
|
||||
currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.Elapsed.Ticks * TimeFactor)) % blendedMaxDuration);
|
||||
currentTicks = blendedMaxDuration == 0 ? TimeSpan.Zero : TimeSpan.FromTicks((currentTicks.Ticks + (long)(time.WarpElapsed.Ticks * TimeFactor)) % blendedMaxDuration);
|
||||
|
||||
currentTime = (currentTicks.Ticks/(double) blendedMaxDuration);
|
||||
}
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public class ##Scriptname## : SyncScript
|
||||
// Condition 2 - the trigger can be a time interval
|
||||
if (TimeInterval > 0)
|
||||
{
|
||||
timeIntervalCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
timeIntervalCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
if (timeIntervalCountdown <= 0f)
|
||||
{
|
||||
timeIntervalCountdown = TimeInterval;
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ public class ##Scriptname## : SyncScript
|
||||
var rotationDelta = Input.MouseDelta;
|
||||
|
||||
// Compute translation speed according to framerate and modifiers
|
||||
var translationSpeed = Speed * (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
var translationSpeed = Speed * (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
|
||||
// Take shortest path
|
||||
var deltaPitch = desiredPitch - pitch;
|
||||
@@ -109,7 +109,7 @@ public class ##Scriptname## : SyncScript
|
||||
desiredYaw = yaw + deltaYaw;
|
||||
|
||||
// Perform orientation transition
|
||||
var rotationAdaptation = (float)Game.UpdateTime.Elapsed.TotalSeconds * RotationAdaptationSpeed;
|
||||
var rotationAdaptation = (float)Game.UpdateTime.WarpElapsed.TotalSeconds * RotationAdaptationSpeed;
|
||||
yaw = Math.Abs(deltaYaw) < rotationAdaptation ? desiredYaw : yaw + rotationAdaptation * Math.Sign(deltaYaw);
|
||||
pitch = Math.Abs(deltaPitch) < rotationAdaptation ? desiredPitch : pitch + rotationAdaptation * Math.Sign(deltaPitch);
|
||||
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ public class ##Scriptname## : AsyncScript
|
||||
|
||||
if (TimeInterval > 0)
|
||||
{
|
||||
timeIntervalCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
timeIntervalCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
if (timeIntervalCountdown <= 0f)
|
||||
{
|
||||
timeIntervalCountdown = TimeInterval;
|
||||
@@ -115,7 +115,7 @@ public class ##Scriptname## : AsyncScript
|
||||
while (secondsCountdown > 0f)
|
||||
{
|
||||
await Script.NextFrame();
|
||||
secondsCountdown -= (float)Game.UpdateTime.Elapsed.TotalSeconds;
|
||||
secondsCountdown -= (float)Game.UpdateTime.WarpElapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
// Remove
|
||||
|
||||
+76
-14
@@ -4,11 +4,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
using Stride.Assets.Templates;
|
||||
using Stride.Core.Assets.Templates;
|
||||
using Stride.Core.Presentation.Controls;
|
||||
using SDDialogResult = Stride.Core.Presentation.Services.DialogResult;
|
||||
using Stride.Core;
|
||||
@@ -59,7 +61,12 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow
|
||||
public IReadOnlyList<string> SelectedAssetPacks =>
|
||||
assetPackChecks.Where(t => t.CheckBox.IsChecked == true).Select(t => t.Identity).ToList();
|
||||
|
||||
public DotNetNewTemplateParametersWindow(ITemplateInfo template, IReadOnlyList<ITemplateInfo>? assetPacks = null)
|
||||
/// <param name="template">The template whose parameters are asked.</param>
|
||||
/// <param name="assetPacks">
|
||||
/// The asset packs to offer, null for none. While it is not complete (the AssetPacks package is downloading),
|
||||
/// the packs row shows the download progress; it is filled in once the task completes.
|
||||
/// </param>
|
||||
public DotNetNewTemplateParametersWindow(ITemplateInfo template, Task<IReadOnlyList<ITemplateInfo>>? assetPacks = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(template);
|
||||
TemplateName = template.Name ?? template.Identity;
|
||||
@@ -72,8 +79,71 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow
|
||||
DescriptionTextBlock.Visibility = Visibility.Collapsed;
|
||||
|
||||
BuildControls(template);
|
||||
if (assetPacks is { Count: > 0 })
|
||||
ParametersPanel.Children.Add(BuildAssetPacksRow(assetPacks));
|
||||
if (assetPacks is not null)
|
||||
AddAssetPacksRow(assetPacks);
|
||||
}
|
||||
|
||||
private async void AddAssetPacksRow(Task<IReadOnlyList<ITemplateInfo>> assetPacks)
|
||||
{
|
||||
var row = new StackPanel { Margin = new Thickness(0, 6, 0, 6) };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Asset packs — Ready-made asset collections added to your project",
|
||||
Margin = new Thickness(0, 0, 0, 3),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
ParametersPanel.Children.Add(row);
|
||||
|
||||
// Until the packs are ready (the AssetPacks package is downloading), show the download.
|
||||
var status = new TextBlock { TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 3) };
|
||||
var progress = new ProgressBar { Height = 6, Minimum = 0, Maximum = 1, IsIndeterminate = true };
|
||||
void Show()
|
||||
{
|
||||
var download = TemplateDownloads.Current;
|
||||
status.Text = download is null ? "Preparing the asset packs..." : TemplateDownloads.Describe(download);
|
||||
// Indeterminate until the download's size is known.
|
||||
progress.IsIndeterminate = download?.Fraction is null;
|
||||
progress.Value = download?.Fraction ?? 0;
|
||||
}
|
||||
void OnChanged() => _ = Dispatcher.InvokeAsync(Show);
|
||||
if (!assetPacks.IsCompleted)
|
||||
{
|
||||
row.Children.Add(status);
|
||||
row.Children.Add(progress);
|
||||
Show();
|
||||
TemplateDownloads.Changed += OnChanged;
|
||||
}
|
||||
|
||||
IReadOnlyList<ITemplateInfo> packs;
|
||||
try
|
||||
{
|
||||
packs = await assetPacks;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Same as an empty list: no packs offered. Other failures are already reported as an empty list by
|
||||
// GetAssetPackTemplatesAsync, so anything else escaping is a bug worth a crash report.
|
||||
packs = [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
TemplateDownloads.Changed -= OnChanged;
|
||||
row.Children.Remove(status);
|
||||
row.Children.Remove(progress);
|
||||
}
|
||||
|
||||
if (packs.Count == 0)
|
||||
{
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "No asset packs are available. The AssetPacks package could not be downloaded.",
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
});
|
||||
return;
|
||||
}
|
||||
foreach (var check in BuildAssetPackChecks(packs))
|
||||
row.Children.Add(check);
|
||||
}
|
||||
|
||||
private void BuildControls(ITemplateInfo template)
|
||||
@@ -240,19 +310,12 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkbox list for the optional asset packs (Building blocks, Materials, ...), one per
|
||||
/// Checkboxes for the optional asset packs (Building blocks, Materials, ...), one per
|
||||
/// item template of the AssetPacks package. All unchecked by default; each selected pack is
|
||||
/// instantiated into the generated game library after the main template.
|
||||
/// </summary>
|
||||
private UIElement BuildAssetPacksRow(IReadOnlyList<ITemplateInfo> assetPacks)
|
||||
private IEnumerable<CheckBox> BuildAssetPackChecks(IReadOnlyList<ITemplateInfo> assetPacks)
|
||||
{
|
||||
var row = new StackPanel { Margin = new Thickness(0, 6, 0, 6) };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Asset packs — Ready-made asset collections added to your project",
|
||||
Margin = new Thickness(0, 0, 0, 3),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
foreach (var pack in assetPacks)
|
||||
{
|
||||
var label = string.IsNullOrEmpty(pack.Description) ? pack.Name : $"{pack.Name} — {pack.Description}";
|
||||
@@ -262,10 +325,9 @@ public partial class DotNetNewTemplateParametersWindow : ModalWindow
|
||||
IsChecked = false,
|
||||
Margin = new Thickness(0, 2, 0, 2),
|
||||
};
|
||||
row.Children.Add(item);
|
||||
assetPackChecks.Add((pack.Identity, item));
|
||||
yield return item;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private static bool ParseBool(string? s) => bool.TryParse(s, out var b) && b;
|
||||
|
||||
+2
-5
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
using Stride.Assets.Templates;
|
||||
@@ -15,10 +14,8 @@ internal sealed class WpfDotNetNewParameterPrompt : IDotNetNewParameterPrompt
|
||||
public async Task<DotNetNewPromptResult?> PromptAsync(ITemplateInfo template, TemplateDotNetNewDescription description)
|
||||
{
|
||||
// Templates that opt into asset packs get the pack checkboxes; the bridge downloads the
|
||||
// AssetPacks package on first use. Empty list (e.g. offline) = no packs section.
|
||||
IReadOnlyList<ITemplateInfo> assetPacks = [];
|
||||
if (description.OffersAssetPacks)
|
||||
assetPacks = await DotNetNewTemplateBridge.GetAssetPackTemplatesAsync().ConfigureAwait(true);
|
||||
// AssetPacks package on first use, while the window is already open. Empty list (e.g. offline) = no packs.
|
||||
var assetPacks = description.OffersAssetPacks ? DotNetNewTemplateBridge.GetAssetPackTemplatesAsync() : null;
|
||||
|
||||
var dialog = new DotNetNewTemplateParametersWindow(template, assetPacks);
|
||||
var result = await dialog.ShowModal();
|
||||
|
||||
+84
-9
@@ -26,6 +26,8 @@ namespace Stride.Core.Assets.Editor.Components.TemplateDescriptions.ViewModels
|
||||
private readonly IModalDialog dialog;
|
||||
private readonly TemplateDescriptionGroupViewModel recentGroup;
|
||||
private readonly TemplateDescriptionGroupViewModel rootGroup;
|
||||
private readonly TemplateDescriptionGroupViewModel defaultGroup;
|
||||
private readonly HashSet<TemplateDescription> listedTemplates = new(ReferenceEqualityComparer.Instance);
|
||||
private string solutionName;
|
||||
private UDirectory solutionLocation;
|
||||
private bool arePropertiesValid;
|
||||
@@ -37,16 +39,15 @@ namespace Stride.Core.Assets.Editor.Components.TemplateDescriptions.ViewModels
|
||||
rootGroup = new TemplateDescriptionGroupViewModel(serviceProvider, "New project");
|
||||
|
||||
// Add a default General group
|
||||
var defaultGroup = new TemplateDescriptionGroupViewModel(rootGroup, "General");
|
||||
defaultGroup = new TemplateDescriptionGroupViewModel(rootGroup, "General");
|
||||
AddNewTemplates();
|
||||
|
||||
foreach (TemplateDescription template in TemplateManager.FindTemplates(TemplateScope.Session))
|
||||
{
|
||||
if (template.Id == StrideLibraryTemplateId)
|
||||
continue;
|
||||
var viewModel = new PackageTemplateViewModel(serviceProvider, template);
|
||||
var group = ProcessGroup(rootGroup, template.Group) ?? defaultGroup;
|
||||
group.Templates.Add(viewModel);
|
||||
}
|
||||
// Template packages still downloading at startup: show their progress, list their templates once ready.
|
||||
TemplateManager.PackagesChanged += OnTemplatePackagesChanged;
|
||||
TemplateDownloads.Changed += OnTemplateDownloadsChanged;
|
||||
UpdateDownloadStatus();
|
||||
DependentProperties.Add(nameof(SelectedGroup), [nameof(ShowDownloadStatus)]);
|
||||
DependentProperties.Add(nameof(DownloadStatus), [nameof(ShowDownloadStatus)]);
|
||||
|
||||
recentGroup = new TemplateDescriptionGroupViewModel(serviceProvider, "Recent projects");
|
||||
foreach (var file in EditorViewModel.Instance.RecentFiles)
|
||||
@@ -75,6 +76,80 @@ namespace Stride.Core.Assets.Editor.Components.TemplateDescriptions.ViewModels
|
||||
|
||||
public ICommandBase BrowseForExistingProjectCommand { get; private set; }
|
||||
|
||||
/// <summary>The template download in progress (or failed), null when there is none.</summary>
|
||||
public string DownloadStatus { get; private set { SetValue(ref field, value); } }
|
||||
|
||||
/// <summary>Whether <see cref="DownloadStatus"/> is shown: only while new project templates are listed, where the downloads add templates.</summary>
|
||||
public bool ShowDownloadStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
if (DownloadStatus is null)
|
||||
return false;
|
||||
for (var group = SelectedGroup; group is not null; group = group.Parent)
|
||||
{
|
||||
if (group == rootGroup)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether a template download is in progress (<see cref="DownloadStatus"/> may also report a failure).</summary>
|
||||
public bool IsDownloading { get; private set { SetValue(ref field, value); } }
|
||||
|
||||
/// <summary>Progress of the download from 0 to 1 (0 while its size is not known).</summary>
|
||||
public double DownloadProgress { get; private set { SetValue(ref field, value); } }
|
||||
|
||||
/// <summary>Whether the size of the download is not known yet (no progress to show).</summary>
|
||||
public bool IsDownloadSizeUnknown { get; private set { SetValue(ref field, value); } } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Destroy()
|
||||
{
|
||||
TemplateManager.PackagesChanged -= OnTemplatePackagesChanged;
|
||||
TemplateDownloads.Changed -= OnTemplateDownloadsChanged;
|
||||
base.Destroy();
|
||||
}
|
||||
|
||||
private void AddNewTemplates()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
var added = false;
|
||||
foreach (TemplateDescription template in TemplateManager.FindTemplates(TemplateScope.Session))
|
||||
{
|
||||
if (template.Id == StrideLibraryTemplateId || !listedTemplates.Add(template))
|
||||
continue;
|
||||
var viewModel = new PackageTemplateViewModel(ServiceProvider, template);
|
||||
var group = ProcessGroup(rootGroup, template.Group) ?? defaultGroup;
|
||||
group.Templates.Add(viewModel);
|
||||
added = true;
|
||||
}
|
||||
if (added && SelectedGroup is not null)
|
||||
{
|
||||
// Refreshing the list clears the selection in the view: keep the user's choice.
|
||||
var selected = SelectedTemplate;
|
||||
UpdateTemplateList();
|
||||
SelectedTemplate = selected;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTemplatePackagesChanged() => Dispatcher.InvokeAsync(AddNewTemplates);
|
||||
|
||||
private void OnTemplateDownloadsChanged() => Dispatcher.InvokeAsync(UpdateDownloadStatus);
|
||||
|
||||
private void UpdateDownloadStatus()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
var download = TemplateDownloads.Current;
|
||||
DownloadStatus = download is null ? null : TemplateDownloads.Describe(download);
|
||||
IsDownloading = download is { Failed: false };
|
||||
DownloadProgress = download?.Fraction ?? 0;
|
||||
IsDownloadSizeUnknown = download?.Fraction is null;
|
||||
}
|
||||
|
||||
public override bool ValidateProperties(out string error)
|
||||
{
|
||||
if (SelectedTemplate is TemplateDescriptionViewModel)
|
||||
|
||||
+9
@@ -40,6 +40,15 @@
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<!-- Template packages downloading in the background (their templates are added to the list once ready). -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Margin="20,10,20,0"
|
||||
Visibility="{Binding ShowDownloadStatus, Converter={sd:VisibleOrCollapsed}}">
|
||||
<TextBlock Text="{Binding DownloadStatus}" TextWrapping="Wrap"/>
|
||||
<!-- Indeterminate until the download's size is known. -->
|
||||
<ProgressBar Height="6" Margin="0,4,0,0" Minimum="0" Maximum="1"
|
||||
Value="{Binding DownloadProgress, Mode=OneWay}" IsIndeterminate="{Binding IsDownloadSizeUnknown}"
|
||||
Visibility="{Binding IsDownloading, Converter={sd:VisibleOrCollapsed}}"/>
|
||||
</StackPanel>
|
||||
<Grid IsEnabled="{Binding SelectedTemplate, Converter={sd:MatchType}, ConverterParameter={x:Type viewModels:TemplateDescriptionViewModel}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
|
||||
@@ -479,12 +479,14 @@ namespace Stride.Core.Assets.Editor.ViewModel
|
||||
{
|
||||
// Snapshot every file the upgrade overwrites into a timestamped backup folder under the solution.
|
||||
// Copy-on-write, so it costs nothing unless an upgrade actually runs and modifies files. The dialog
|
||||
// checkbox below can opt out; the callback updates this flag, which the session honors.
|
||||
// checkbox below can opt out; the callback updates this flag, which the session honors. The choice
|
||||
// covers the whole upgrade: only the first dialog can change it, later ones show it greyed out.
|
||||
var loadParameters = new PackageLoadParameters
|
||||
{
|
||||
CancelToken = cancellationSource.Token,
|
||||
BackupBeforeUpgrade = true,
|
||||
};
|
||||
var backupChoiceMade = false;
|
||||
loadParameters.PackageUpgradeRequested = (package, pendingUpgrades) =>
|
||||
{
|
||||
// Generate message (in markdown, so we need to double line feeds)
|
||||
@@ -507,10 +509,11 @@ namespace Stride.Core.Assets.Editor.ViewModel
|
||||
new DialogButtonInfo { Content = Tr._p("Button", "Skip"), Result = (int)PackageUpgradeRequestedAnswer.DoNotUpgrade },
|
||||
};
|
||||
var applyToAll = new DialogCheckBoxInfo { Content = Tr._p("Message", "Do this for every package in the solution"), IsChecked = false };
|
||||
var backup = new DialogCheckBoxInfo { Content = Tr._p("Message", "Back up each modified file to a timestamped folder under the solution"), IsChecked = true };
|
||||
var backup = new DialogCheckBoxInfo { Content = Tr._p("Message", "Back up each modified file to a timestamped folder under the solution"), IsChecked = loadParameters.BackupBeforeUpgrade, IsEnabled = !backupChoiceMade };
|
||||
var buttonResult = workProgress.ServiceProvider.Get<IDialogService>().CheckedMessageBoxAsync(message.ToString(), [applyToAll, backup], buttons).Result;
|
||||
|
||||
loadParameters.BackupBeforeUpgrade = backup.IsChecked == true;
|
||||
backupChoiceMade = true;
|
||||
var result = (PackageUpgradeRequestedAnswer)buttonResult;
|
||||
if (applyToAll.IsChecked == true)
|
||||
{
|
||||
|
||||
@@ -431,6 +431,7 @@ public static class Program
|
||||
var viewModel = new NewOrOpenSessionTemplateCollectionViewModel(serviceProvider, startupWindow);
|
||||
startupWindow.Templates = viewModel;
|
||||
startupWindow.ShowDialog();
|
||||
viewModel.Destroy();
|
||||
|
||||
// The user selected a template to instantiate
|
||||
if (startupWindow.NewSessionParameters != null)
|
||||
|
||||
@@ -97,15 +97,22 @@
|
||||
<!-- NuGet dependencies (and build-ordering triggers) on the three template packages, so a Stride
|
||||
install pulls them in. Private=false alone emits a content-only nuspec <dependency> (the templates
|
||||
produce no assembly to reference) — same idiom as the Stride.Core marker dep in
|
||||
Stride.Templates.Common.targets. ReferenceOutputAssembly=false would suppress the dependency. -->
|
||||
Stride.Templates.Common.targets. ReferenceOutputAssembly=false would suppress the dependency.
|
||||
Games is engine-versioned and packed with every build. Starters and Samples are content-versioned
|
||||
(sources/templates/StrideSamplesVersion.props): a normal build does not pack them and the dependency
|
||||
names the exact published version (pinned below); with StridePackContentTemplates=true it follows the
|
||||
produced -devN pack. A PackageReference would not do: restore would fail before the first publish of a new
|
||||
version. -->
|
||||
<ProjectReference Include="..\..\templates\Stride.Templates.Games\Stride.Templates.Games.csproj">
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\templates\Stride.Templates.Games.Starters\Stride.Templates.Games.Starters.csproj">
|
||||
<Private>false</Private>
|
||||
<StrideExactDependency>true</StrideExactDependency>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\templates\Stride.Templates.Samples\Stride.Templates.Samples.csproj">
|
||||
<Private>false</Private>
|
||||
<StrideExactDependency>true</StrideExactDependency>
|
||||
</ProjectReference>
|
||||
<!-- Build-ordering only (no nuspec dependency): asset packs are fetched on demand by the New
|
||||
Game dialog, not pulled at install time. The reference just makes a dev GameStudio build
|
||||
@@ -116,6 +123,22 @@
|
||||
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<!-- NuGet records a ProjectReference dependency as ">= version"; the content template packs are pinned to the
|
||||
exact version ("[version]") instead, so an install fails loudly when it is missing from the feed rather than
|
||||
silently resolving to a later content version the engine did not ship with. NuGet's cross-project version
|
||||
query fills _ProjectReferencesWithVersions right before the nuspec is written; the pinned ones are marked
|
||||
StrideExactDependency above. -->
|
||||
<Target Name="StridePinExactTemplateDependencies" AfterTargets="_GetProjectReferenceVersions">
|
||||
<PropertyGroup>
|
||||
<_StrideExactDependencyNames>;@(ProjectReference->WithMetadataValue('StrideExactDependency', 'true')->'%(Filename)');</_StrideExactDependencyNames>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<_ProjectReferencesWithVersions Condition="$(_StrideExactDependencyNames.Contains(';%(_ProjectReferencesWithVersions.Filename);')) And !$([System.String]::Copy('%(_ProjectReferencesWithVersions.ProjectVersion)').StartsWith('['))">
|
||||
<ProjectVersion>[%(_ProjectReferencesWithVersions.ProjectVersion)]</ProjectVersion>
|
||||
</_ProjectReferencesWithVersions>
|
||||
</ItemGroup>
|
||||
<Message Importance="low" Text="Template dependencies: @(_ProjectReferencesWithVersions->WithMetadataValue('Extension', '.csproj')->'%(Filename) %(ProjectVersion)', ', ')" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\..\BACKERS.md" Link="BACKERS.md">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<Compile Include="TestAssetContentTypeMap.cs" />
|
||||
<Compile Include="TestAssetUrlConstantsGenerator.cs" />
|
||||
<Compile Include="TestCodeUpgrade.cs" />
|
||||
<Compile Include="TestContentTemplateResolver.cs" />
|
||||
<Compile Include="TestMemberRequiredComponentChecks.cs" />
|
||||
<Compile Include="XunitAttributes.cs" />
|
||||
<Compile Include="TestMaterialGenerator.cs" />
|
||||
|
||||
@@ -363,4 +363,110 @@ public class TestCodeUpgrade
|
||||
|
||||
Assert.Contains("v.IsSRgb(2)", result);
|
||||
}
|
||||
|
||||
// The #1715 shape: memory helpers left Utilities for MemoryUtilities (same namespace), some renamed.
|
||||
private const string MovedMembersSource = """
|
||||
namespace TestNs
|
||||
{
|
||||
static class Old
|
||||
{
|
||||
public static nint AllocateMemory(int sizeInBytes, int align = 16) => 0;
|
||||
public static void FreeMemory(nint buffer) { }
|
||||
public static void Swap<T>(ref T left, ref T right) { }
|
||||
public static void Dispose(object o) { }
|
||||
}
|
||||
static class Other
|
||||
{
|
||||
public static void FreeMemory(nint buffer) { }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private static readonly CodeUpgrade MoveUpgrade = Rewrite(
|
||||
StaticMemberMove("TestNs.Old", "AllocateMemory", "TestNs.New", "Allocate"),
|
||||
StaticMemberMove("TestNs.Old", "FreeMemory", "TestNs.New", "Free"),
|
||||
StaticMemberMove("TestNs.Old", "Swap", "TestNs.New"),
|
||||
ParameterRename("TestNs.Old", "AllocateMemory", "align", "alignment"));
|
||||
|
||||
[Fact]
|
||||
public async Task StaticMemberMoveRetargetsOnlyTheMatchingSymbol()
|
||||
{
|
||||
var source = MovedMembersSource + """
|
||||
|
||||
namespace TestNs
|
||||
{
|
||||
class Usage
|
||||
{
|
||||
void A(nint p) => Old.FreeMemory(p);
|
||||
void B(nint p) => Other.FreeMemory(p);
|
||||
void C(object o) => Old.Dispose(o);
|
||||
void D(nint p) => TestNs.Old.FreeMemory(p);
|
||||
void E(nint p) => global::TestNs.Old.FreeMemory(p);
|
||||
System.Action<nint> F() => Old.FreeMemory;
|
||||
string G() => nameof(Old.FreeMemory);
|
||||
void H(ref int a, ref int b) => Old.Swap<int>(ref a, ref b);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = await ApplyAsync(source, MoveUpgrade);
|
||||
|
||||
Assert.Contains("=> New.Free(p);", result);
|
||||
// The qualified receiver keeps its shape.
|
||||
Assert.Contains("=> TestNs.New.Free(p);", result);
|
||||
Assert.Contains("=> global::TestNs.New.Free(p);", result);
|
||||
// Method groups, nameof and generic names are references too.
|
||||
Assert.Contains("=> New.Free;", result);
|
||||
Assert.Contains("nameof(New.Free)", result);
|
||||
Assert.Contains("New.Swap<int>(ref a, ref b)", result);
|
||||
// The unrelated same-named member and the members that stayed are untouched.
|
||||
Assert.Contains("Other.FreeMemory(p)", result);
|
||||
Assert.Contains("Old.Dispose(o)", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StaticMemberMoveComposesWithParameterRename()
|
||||
{
|
||||
// Both rewrites hit the same reference (the method name) but edit disjoint nodes.
|
||||
var source = MovedMembersSource + """
|
||||
|
||||
namespace TestNs
|
||||
{
|
||||
class Usage
|
||||
{
|
||||
nint A() => Old.AllocateMemory(64, align: 32);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = await ApplyAsync(source, MoveUpgrade);
|
||||
|
||||
Assert.Contains("New.Allocate(64, alignment: 32)", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StaticMemberMoveQualifiesAliasAndUsingStaticReferences()
|
||||
{
|
||||
// Neither form names the old type at the call site, so the new type can't be assumed in scope.
|
||||
var source = """
|
||||
using static TestNs.Old;
|
||||
using Alias = TestNs.Old;
|
||||
|
||||
""" + MovedMembersSource + """
|
||||
|
||||
namespace UserNs
|
||||
{
|
||||
class Usage
|
||||
{
|
||||
void A(nint p) => FreeMemory(p);
|
||||
void B(nint p) => Alias.FreeMemory(p);
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = await ApplyAsync(source, MoveUpgrade);
|
||||
|
||||
Assert.Contains("void A(nint p) => TestNs.New.Free(p);", result);
|
||||
Assert.Contains("void B(nint p) => TestNs.New.Free(p);", result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
#nullable enable
|
||||
using Stride.Assets.Templates;
|
||||
using Stride.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Stride.Assets.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// The exact-version rules shared by the Game Studio bridge and the stride CLI to pick a content template package.
|
||||
/// </summary>
|
||||
public class TestContentTemplateResolver
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("4.4.0-dev4", "dev4")]
|
||||
[InlineData("4.4.0-dev", "dev")]
|
||||
[InlineData("4.4.0-beta8-dev4", "dev4")]
|
||||
[InlineData("4.4.0", null)]
|
||||
[InlineData("4.4.0-beta8", null)]
|
||||
[InlineData("4.4.0-devices1", null)]
|
||||
public void DevSuffixOf(string host, string? expected)
|
||||
=> Assert.Equal(expected, ContentTemplateResolver.DevSuffixOf(new PackageVersion(host)));
|
||||
|
||||
[Theory]
|
||||
[InlineData("4.4.0-beta7", "4.4.0-dev4", "4.4.0-beta7-dev4")]
|
||||
[InlineData("4.4.0", "4.4.0-beta8-dev2", "4.4.0-dev2")]
|
||||
[InlineData("4.4.0", "4.4.0-beta8", null)]
|
||||
public void DevPackVersion(string content, string host, string? expected)
|
||||
=> Assert.Equal(expected is null ? null : new PackageVersion(expected), ContentTemplateResolver.DevPackVersion(new PackageVersion(content), new PackageVersion(host)));
|
||||
|
||||
[Theory]
|
||||
[InlineData("4.4.0-beta7-dev4", "4.4.0-dev4", "4.4.0-beta7")]
|
||||
[InlineData("4.4.0-dev4", "4.4.0-beta8-dev4", "4.4.0")]
|
||||
[InlineData("4.4.0-beta7", "4.4.0-dev4", "4.4.0-beta7")]
|
||||
[InlineData("4.4.0-beta7-dev2", "4.4.0-dev4", "4.4.0-beta7-dev2")]
|
||||
[InlineData("4.4.1", "4.4.1", "4.4.1")]
|
||||
public void ContentVersionFromPin(string pinned, string host, string expected)
|
||||
=> Assert.Equal(new PackageVersion(expected), ContentTemplateResolver.ContentVersionFromPin(new PackageVersion(pinned), new PackageVersion(host)));
|
||||
|
||||
[Theory]
|
||||
[InlineData("4.4.0-beta7", true)]
|
||||
[InlineData("4.4.0-beta7-dev4", true)]
|
||||
[InlineData("4.4.0-beta7-dev3", false)]
|
||||
[InlineData("4.4.0-dev4", false)]
|
||||
[InlineData("4.4.1-dev4", false)]
|
||||
[InlineData("4.4.0", false)]
|
||||
public void IsAcceptableForDevHost(string candidate, bool expected)
|
||||
=> Assert.Equal(expected, ContentTemplateResolver.IsAcceptable(new PackageVersion(candidate), new PackageVersion("4.4.0-beta7"), new PackageVersion("4.4.0-dev4")));
|
||||
|
||||
[Theory]
|
||||
[InlineData("4.4.0", true)]
|
||||
[InlineData("4.4.0-dev4", false)]
|
||||
[InlineData("4.4.1", false)]
|
||||
public void IsAcceptableForReleaseHost(string candidate, bool expected)
|
||||
=> Assert.Equal(expected, ContentTemplateResolver.IsAcceptable(new PackageVersion(candidate), new PackageVersion("4.4.0"), new PackageVersion("4.4.0")));
|
||||
|
||||
[Fact]
|
||||
public void LocalBuildRange()
|
||||
{
|
||||
// A dev host takes its own pack from local feeds, a release host the plain version; both exactly.
|
||||
AssertExactRange("4.4.0-beta7-dev4", ContentTemplateResolver.LocalBuildRange(new PackageVersion("4.4.0-beta7"), new PackageVersion("4.4.0-dev4")));
|
||||
AssertExactRange("4.4.0", ContentTemplateResolver.LocalBuildRange(new PackageVersion("4.4.0"), new PackageVersion("4.4.0")));
|
||||
}
|
||||
|
||||
private static void AssertExactRange(string version, PackageVersionRange range)
|
||||
{
|
||||
Assert.Equal(new PackageVersion(version), range.MinVersion);
|
||||
Assert.Equal(new PackageVersion(version), range.MaxVersion);
|
||||
Assert.True(range.IsMinInclusive);
|
||||
Assert.True(range.IsMaxInclusive);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,69 @@ public static class CodeUpgrades
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrates a static member that moved to another (non-nested) type, optionally renamed:
|
||||
/// <c>Utilities.FreeMemory(p)</c> → <c>MemoryUtilities.Free(p)</c>. Alias and <c>using static</c>
|
||||
/// references become fully qualified, since the new type can't be assumed in scope.
|
||||
/// </summary>
|
||||
public static SymbolRewrite StaticMemberMove(string declaringType, string memberName, string newDeclaringType, string newMemberName = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(declaringType);
|
||||
ArgumentNullException.ThrowIfNull(memberName);
|
||||
ArgumentNullException.ThrowIfNull(newDeclaringType);
|
||||
newMemberName ??= memberName;
|
||||
|
||||
var oldTypeName = declaringType[(declaringType.LastIndexOf('.') + 1)..];
|
||||
var newTypeName = newDeclaringType[(newDeclaringType.LastIndexOf('.') + 1)..];
|
||||
var sameNamespace = declaringType[..^oldTypeName.Length] == newDeclaringType[..^newTypeName.Length];
|
||||
|
||||
return new SymbolRewrite(
|
||||
compilation => ResolveMembers(compilation, declaringType, memberName),
|
||||
(editor, referenceNode, symbol) =>
|
||||
{
|
||||
// Anything that isn't the member-name identifier (e.g. a reference inside a doc comment,
|
||||
// which the engine doesn't descend into) is left alone.
|
||||
if (referenceNode is not SimpleNameSyntax name || name.Identifier.ValueText != memberName)
|
||||
return;
|
||||
|
||||
// WithIdentifier keeps the type argument list of a generic name (Swap<T>).
|
||||
var newName = name.WithIdentifier(SyntaxFactory.Identifier(name.Identifier.LeadingTrivia, newMemberName, name.Identifier.TrailingTrivia));
|
||||
|
||||
if (name.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == name)
|
||||
{
|
||||
var receiver = memberAccess.Expression;
|
||||
var newReceiver = receiver;
|
||||
if (editor.SemanticModel.GetSymbolInfo(receiver).Symbol is ITypeSymbol)
|
||||
{
|
||||
var receiverTypeName = receiver switch
|
||||
{
|
||||
MemberAccessExpressionSyntax qualified => qualified.Name,
|
||||
AliasQualifiedNameSyntax aliasQualified => aliasQualified.Name,
|
||||
_ => receiver as SimpleNameSyntax,
|
||||
};
|
||||
newReceiver = sameNamespace && receiverTypeName is not null && receiverTypeName.Identifier.ValueText == oldTypeName
|
||||
? receiver.ReplaceNode(receiverTypeName, SyntaxFactory.IdentifierName(newTypeName).WithTriviaFrom(receiverTypeName))
|
||||
: SyntaxFactory.ParseExpression(newDeclaringType).WithTriviaFrom(receiver);
|
||||
}
|
||||
editor.ReplaceNode(memberAccess, memberAccess.WithExpression(newReceiver).WithName(newName));
|
||||
}
|
||||
else if (name.Parent is not MemberBindingExpressionSyntax)
|
||||
{
|
||||
// Bare reference through `using static OldType;`, which doesn't bring the new type
|
||||
// (or even its namespace) in scope.
|
||||
var qualified = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
SyntaxFactory.ParseExpression(newDeclaringType),
|
||||
newName.WithoutTrivia());
|
||||
editor.ReplaceNode(name, qualified.WithTriviaFrom(name));
|
||||
}
|
||||
else
|
||||
{
|
||||
editor.ReplaceNode(name, newName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the argument list the referenced member is called with, and yields the
|
||||
/// <see cref="NameColonSyntax"/> of each named argument in it. Reference forms without an argument
|
||||
|
||||
@@ -87,12 +87,13 @@ internal static class SymbolRewriteEngine
|
||||
continue;
|
||||
|
||||
var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
|
||||
var appliedSpans = new HashSet<TextSpan>();
|
||||
var appliedSpans = new HashSet<(TextSpan, SymbolRewrite)>();
|
||||
foreach (var edit in edits)
|
||||
{
|
||||
// A node can be matched by multiple symbols (e.g. extension skeleton + implementation);
|
||||
// apply each span once.
|
||||
if (!appliedSpans.Add(edit.Span))
|
||||
// apply each rewrite once per span. Distinct rewrites may share a span (a moved method
|
||||
// that also had a parameter renamed), as long as they edit disjoint nodes.
|
||||
if (!appliedSpans.Add((edit.Span, edit.Rewrite)))
|
||||
continue;
|
||||
var node = root.FindNode(edit.Span, getInnermostNodeForTie: true);
|
||||
if (node is null)
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == '$(StrideFramework)'">
|
||||
<DefineConstants>STRIDE_VIDEO_FFMPEG;$(DefineConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<!-- The content version this engine names, read by DotNetNewTemplateBridge: the committed file itself (not a
|
||||
-p:StrideSamplesVersion= override, which only names what a samples release is packing). A resource, not an
|
||||
assembly attribute, so a new version stays out of the reference assembly and dependents don't rebuild. -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(StrideRoot)sources\templates\StrideSamplesVersion.props"
|
||||
LogicalName="StrideSamplesVersion.props" Link="Templates\StrideSamplesVersion.props" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.StringTools" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Build.Framework" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
|
||||
@@ -104,6 +104,21 @@ namespace Stride.Assets
|
||||
Rewrite(
|
||||
ParameterRename("Stride.Graphics.IndexBufferBinding", ".ctor", "count", "indexCount")),
|
||||
|
||||
// 4.4: the memory helpers of Stride.Core.Utilities moved to Stride.Core.MemoryUtilities,
|
||||
// some renamed along with their parameters (#1715).
|
||||
Rewrite(
|
||||
StaticMemberMove("Stride.Core.Utilities", "CopyWithAlignmentFallback", "Stride.Core.MemoryUtilities"),
|
||||
StaticMemberMove("Stride.Core.Utilities", "Clear", "Stride.Core.MemoryUtilities"),
|
||||
StaticMemberMove("Stride.Core.Utilities", "Swap", "Stride.Core.MemoryUtilities"),
|
||||
StaticMemberMove("Stride.Core.Utilities", "AllocateMemory", "Stride.Core.MemoryUtilities", "Allocate"),
|
||||
StaticMemberMove("Stride.Core.Utilities", "AllocateClearedMemory", "Stride.Core.MemoryUtilities", "AllocateCleared"),
|
||||
StaticMemberMove("Stride.Core.Utilities", "FreeMemory", "Stride.Core.MemoryUtilities", "Free"),
|
||||
StaticMemberMove("Stride.Core.Utilities", "IsMemoryAligned", "Stride.Core.MemoryUtilities", "IsAligned"),
|
||||
ParameterRename("Stride.Core.Utilities", "AllocateMemory", "align", "alignment"),
|
||||
ParameterRename("Stride.Core.Utilities", "AllocateClearedMemory", "align", "alignment"),
|
||||
ParameterRename("Stride.Core.Utilities", "IsMemoryAligned", "align", "alignment"),
|
||||
ParameterRename("Stride.Core.Utilities", "IsMemoryAligned", "memoryPtr", "memoryAddress")),
|
||||
|
||||
// 4.4: SharpDX and SharpFont were dropped, Vortice no longer flows to game projects,
|
||||
// and Stride.Core.Shaders was retired by the sdsl rewrite; their leftover (unused)
|
||||
// using directives would no longer compile (#3249).
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Stride.Core;
|
||||
using Stride.Core.Packages;
|
||||
|
||||
namespace Stride.Assets.Templates;
|
||||
|
||||
/// <summary>
|
||||
/// Picks the installed copy of a content template package (Samples, Starters, AssetPacks) for an engine. Shared by
|
||||
/// source between the Game Studio bridge and the stride CLI, so both hosts resolve the same package for the same
|
||||
/// engine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The content packages are versioned independently of the engine: an engine names the exact content version it was
|
||||
/// released with (<c>StrideSamplesVersion</c> in sources/templates/StrideSamplesVersion.props, recorded as
|
||||
/// Stride.GameStudio's pinned dependency), and that is what resolves. The one exception is a developer's own
|
||||
/// checkout: with StridePackContentTemplates, a build with the checkout's <c>-devN</c> suffix packs the content as
|
||||
/// exactly that version plus the suffix
|
||||
/// (<c>4.4.0-beta7-dev4</c>), and that pack wins over the published one (it is what the samples being edited look
|
||||
/// like; the build removes it when the flag is turned off). Only a dev engine looks for it, and only with its own
|
||||
/// suffix, and then does not copy a local build of the published version (see <see cref="LocalBuildRange"/>).
|
||||
/// </remarks>
|
||||
public static class ContentTemplateResolver
|
||||
{
|
||||
public const string SamplesPackageId = "Stride.Templates.Samples";
|
||||
public const string StartersPackageId = "Stride.Templates.Games.Starters";
|
||||
public const string AssetPacksPackageId = "Stride.Templates.AssetPacks";
|
||||
|
||||
/// <summary>The content-versioned template packages, all released together at one content version.</summary>
|
||||
public static readonly IReadOnlyList<string> PackageIds = [SamplesPackageId, StartersPackageId, AssetPacksPackageId];
|
||||
|
||||
/// <summary>
|
||||
/// The package whose pinned dependency on <see cref="SamplesPackageId"/> in Stride.GameStudio names the content
|
||||
/// version of an installed engine (the CLI reads it, since it targets engines other than its own build).
|
||||
/// </summary>
|
||||
public const string ContentVersionDependencyId = SamplesPackageId;
|
||||
|
||||
// The checkout suffix ends the prerelease label: "dev4", or "beta8-dev4" for a dev build of a beta engine.
|
||||
private static readonly Regex DevSuffix = new("(?:^|-)(dev[0-9]*)$", RegexOptions.CultureInvariant);
|
||||
|
||||
public static bool IsContentPackage(string packageId)
|
||||
=> PackageIds.Contains(packageId, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The checkout suffix (<c>dev</c>, <c>dev3</c>, ...) of a development engine build (<c>4.4.0-dev3</c>,
|
||||
/// <c>4.4.0-beta8-dev3</c>), null for a release or prerelease engine. Only a dev host may use a dev content pack.
|
||||
/// </summary>
|
||||
public static string? DevSuffixOf(PackageVersion hostVersion)
|
||||
=> DevSuffix.Match(hostVersion.SpecialVersion ?? string.Empty) is { Success: true } match ? match.Groups[1].Value : null;
|
||||
|
||||
/// <summary>
|
||||
/// The version of this checkout's own pack of <paramref name="contentVersion"/> (<c>4.4.0-beta7-dev4</c>), null
|
||||
/// when <paramref name="hostVersion"/> is not a dev build.
|
||||
/// </summary>
|
||||
public static PackageVersion? DevPackVersion(PackageVersion contentVersion, PackageVersion hostVersion)
|
||||
=> DevSuffixOf(hostVersion) is { } devSuffix ? new PackageVersion($"{contentVersion}-{devSuffix}") : null;
|
||||
|
||||
/// <summary>
|
||||
/// The content version an installed engine names through its pinned dependency <paramref name="pinned"/>. A dev
|
||||
/// engine built with StridePackContentTemplates pins its own dev pack (<c>4.4.0-beta7-dev4</c>); its content
|
||||
/// version is that without the engine's suffix, as the running engine sees it.
|
||||
/// </summary>
|
||||
public static PackageVersion ContentVersionFromPin(PackageVersion pinned, PackageVersion hostVersion)
|
||||
{
|
||||
var devSuffix = DevSuffixOf(hostVersion);
|
||||
var text = pinned.ToString();
|
||||
return devSuffix is not null && text.EndsWith($"-{devSuffix}", StringComparison.OrdinalIgnoreCase)
|
||||
? new PackageVersion(text[..^(devSuffix.Length + 1)])
|
||||
: pinned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether an installed <paramref name="candidate"/> may serve as content version <paramref name="contentVersion"/>
|
||||
/// for the engine <paramref name="hostVersion"/>: exactly that version, or exactly this checkout's pack of it.
|
||||
/// </summary>
|
||||
public static bool IsAcceptable(PackageVersion candidate, PackageVersion contentVersion, PackageVersion hostVersion)
|
||||
=> candidate.Equals(contentVersion) || candidate.Equals(DevPackVersion(contentVersion, hostVersion));
|
||||
|
||||
/// <summary>
|
||||
/// The package among <paramref name="installed"/> to use: this checkout's pack when there is one, else the exact
|
||||
/// content version. Null when neither is installed.
|
||||
/// </summary>
|
||||
public static NugetLocalPackage? Pick(IEnumerable<NugetLocalPackage> installed, PackageVersion contentVersion, PackageVersion hostVersion)
|
||||
{
|
||||
var devPack = DevPackVersion(contentVersion, hostVersion);
|
||||
var candidates = installed.ToList();
|
||||
return candidates.FirstOrDefault(package => package.Version.Equals(devPack))
|
||||
?? candidates.FirstOrDefault(package => package.Version.Equals(contentVersion));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The one version a host takes from local package sources (a checkout's <c>bin/packages</c>, the nugetdev feed),
|
||||
/// as an exact range. A dev host takes only this checkout's pack: a local file carrying the published version
|
||||
/// (same number, maybe different content) is not taken over the published one. A host without a checkout suffix
|
||||
/// (a CI build, or a checkout building the clean version) packs the content under the plain version, and takes
|
||||
/// that local build.
|
||||
/// </summary>
|
||||
public static PackageVersionRange LocalBuildRange(PackageVersion contentVersion, PackageVersion hostVersion)
|
||||
{
|
||||
var version = DevPackVersion(contentVersion, hostVersion) ?? contentVersion;
|
||||
return new PackageVersionRange(version, true, version, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <paramref name="packageId"/> for the engine: picks among the installed copies, and when none is
|
||||
/// acceptable installs the exact content version from the configured sources (once; it then stays in the
|
||||
/// store). Returns null when it is neither installed nor obtainable (offline, or not published yet).
|
||||
/// </summary>
|
||||
/// <param name="installed">
|
||||
/// Lists the installed copies of a package id, first taking the given range from local package sources
|
||||
/// (<see cref="LocalBuildRange"/>).
|
||||
/// </param>
|
||||
/// <param name="install">Installs exactly a version of a package from the sources; null when it could not be obtained.</param>
|
||||
/// <param name="log">Receives one line saying which copy won, or why none did.</param>
|
||||
public static async Task<NugetLocalPackage?> ResolveAsync(
|
||||
string packageId, PackageVersion contentVersion, PackageVersion hostVersion,
|
||||
Func<string, PackageVersionRange, IEnumerable<NugetLocalPackage>> installed,
|
||||
Func<string, PackageVersion, Task<NugetLocalPackage?>> install,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
var localBuildRange = LocalBuildRange(contentVersion, hostVersion);
|
||||
var picked = Pick(installed(packageId, localBuildRange), contentVersion, hostVersion);
|
||||
if (picked is not null)
|
||||
{
|
||||
log?.Invoke(picked.Version.Equals(contentVersion)
|
||||
? $"{packageId}: using the content version {contentVersion} this engine names."
|
||||
: $"{packageId}: using this checkout's pack {picked.Version} over the content version {contentVersion} this engine names.");
|
||||
return picked;
|
||||
}
|
||||
|
||||
log?.Invoke($"{packageId} {contentVersion} is not installed; installing it from the package sources.");
|
||||
NugetLocalPackage? fetched;
|
||||
try
|
||||
{
|
||||
fetched = await install(packageId, contentVersion).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log?.Invoke($"{packageId} {contentVersion} could not be installed: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Re-pick rather than trust the returned object: only the exact version may be used, whatever the install
|
||||
// reports.
|
||||
picked = fetched is null ? null : Pick(installed(packageId, localBuildRange), contentVersion, hostVersion);
|
||||
log?.Invoke(picked is null
|
||||
? $"{packageId} {contentVersion} could not be installed (not published, or no package source reachable)."
|
||||
: $"{packageId}: installed the content version {contentVersion} this engine names.");
|
||||
return picked;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
using Microsoft.TemplateEngine.Edge.Settings;
|
||||
@@ -33,20 +35,18 @@ namespace Stride.Assets.Templates;
|
||||
public static class DotNetNewTemplateBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// Package IDs the bridge tries to resolve via <see cref="PackageStore"/> on startup.
|
||||
/// Only <c>Stride.Templates.Games</c> (NewGame) ships in the GameStudio installer; the
|
||||
/// others are dev-only here (present in <c>%LocalAppData%\stride\nugetdev</c> when the
|
||||
/// solution has been built, absent in installer-only setups). End users reach Starters /
|
||||
/// Samples via the editor's template store (future) or CLI <c>dotnet new install</c>, and
|
||||
/// AssetPacks via the on-demand download in <see cref="GetAssetPackTemplatesAsync"/>; this
|
||||
/// list just controls which packages the bridge proactively probes on startup. Missing
|
||||
/// packages are tolerated (per-package warning, no error).
|
||||
/// Package IDs the bridge resolves on startup. <c>Stride.Templates.Games</c> (NewGame) is engine-versioned and
|
||||
/// installed with Game Studio. The others carry the content version this engine names
|
||||
/// (<see cref="ContentVersion"/>, resolved by <see cref="ContentTemplateResolver"/>): Starters and
|
||||
/// Samples are Game Studio dependencies too, AssetPacks is fetched on demand (also by
|
||||
/// <see cref="GetAssetPackTemplatesAsync"/>). A package that is neither installed nor obtainable is tolerated
|
||||
/// (per-package warning, no error).
|
||||
/// </summary>
|
||||
private static readonly string[] BundledTemplatePackageIds =
|
||||
{
|
||||
"Stride.Templates.Games",
|
||||
"Stride.Templates.Games.Starters",
|
||||
"Stride.Templates.Samples",
|
||||
ContentTemplateResolver.StartersPackageId,
|
||||
ContentTemplateResolver.SamplesPackageId,
|
||||
AssetPacksPackageId,
|
||||
};
|
||||
|
||||
@@ -56,28 +56,45 @@ public static class DotNetNewTemplateBridge
|
||||
/// <see cref="GetAssetPackTemplatesAsync"/> when a game template offering packs is
|
||||
/// instantiated on a machine that doesn't have it yet.
|
||||
/// </summary>
|
||||
public const string AssetPacksPackageId = "Stride.Templates.AssetPacks";
|
||||
public const string AssetPacksPackageId = ContentTemplateResolver.AssetPacksPackageId;
|
||||
|
||||
private static DotNetNewTemplateRegistry? registry;
|
||||
private static readonly object InitLock = new();
|
||||
|
||||
/// <summary>Serializes content downloads (<see cref="InstallContentPackageAsync"/>).</summary>
|
||||
private static readonly SemaphoreSlim DownloadGate = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// In-process singleton; null until <see cref="RegisterProjectTemplates"/> has run.
|
||||
/// Consumed by <c>DotNetNewTemplateGenerator</c> at instantiation time.
|
||||
/// </summary>
|
||||
public static DotNetNewTemplateRegistry? Registry => registry;
|
||||
|
||||
/// <summary>Identities of the templates already registered with <see cref="TemplateManager"/> (guarded by <see cref="InitLock"/>).</summary>
|
||||
private static readonly HashSet<string> RegisteredTemplateIdentities = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves each <see cref="BundledTemplatePackageIds"/> entry via <see cref="PackageStore"/>,
|
||||
/// installs them into the shared registry, and wraps every loaded dotnet new template as a
|
||||
/// <see cref="TemplateDotNetNewDescription"/> registered with <see cref="TemplateManager"/>.
|
||||
/// Tolerates missing packages — dev workflows that haven't built every template project yet
|
||||
/// still load the editor.
|
||||
/// The package holding the descriptions handed to <see cref="TemplateManager"/>. Never saved to disk and never
|
||||
/// backed by a real .sdpkg: its <see cref="Package.Templates"/> is the only thing
|
||||
/// <see cref="TemplateManager.FindTemplates"/> reads. A later pass (the background content download) adds to this
|
||||
/// same package, so the descriptions of one host live in one place. FullPath must be non-null for the
|
||||
/// DistinctPackagePathComparer used in FindTemplates; the sentinel path collides with no real package.
|
||||
/// </summary>
|
||||
private static readonly Package SyntheticPackage = new() { FullPath = new UFile("Stride.DotNetNewTemplates.synthetic") };
|
||||
|
||||
/// <summary>
|
||||
/// Resolves each <see cref="BundledTemplatePackageIds"/> entry installed on this machine, installs them into the
|
||||
/// shared registry, and wraps every loaded dotnet new template as a <see cref="TemplateDotNetNewDescription"/>
|
||||
/// registered with <see cref="TemplateManager"/>. Never downloads: this runs during editor startup, often on the
|
||||
/// UI thread. Starters and Samples missing locally are downloaded in the background and their templates are
|
||||
/// registered when ready (AssetPacks are fetched on demand by <see cref="GetAssetPackTemplatesAsync"/>).
|
||||
/// Tolerates missing packages — dev workflows that haven't built every template project yet still load the editor.
|
||||
/// </summary>
|
||||
public static void RegisterProjectTemplates()
|
||||
{
|
||||
var logger = GlobalLogger.GetLogger("DotNetNewTemplateBridge");
|
||||
logger.ActivateLog(LogMessageType.Info);
|
||||
var missingContent = new List<string>();
|
||||
lock (InitLock)
|
||||
{
|
||||
// Stride-owned settings tree ({profileDir}/.templateengine/...), kept out of the user's
|
||||
@@ -103,85 +120,135 @@ public static class DotNetNewTemplateBridge
|
||||
// can be resolved back to absolute on-disk locations at registration time.
|
||||
var sdtplsByIdentity = new Dictionary<string, TemplateMetadataSource>();
|
||||
foreach (var packageId in BundledTemplatePackageIds)
|
||||
InstallBundledPackage(packageId, sdtplsByIdentity, logger);
|
||||
|
||||
var templatesTask = registry.GetTemplatesAsync();
|
||||
templatesTask.Wait();
|
||||
var templates = templatesTask.Result;
|
||||
logger.Info($"Loaded {templates.Count} dotnet new template(s) total");
|
||||
|
||||
// Synthetic package: holds only the TemplateDescriptions we want TemplateManager to
|
||||
// surface. Never saved to disk, never has a real .sdpkg — the descriptions are the
|
||||
// only thing FindTemplates() reads (it does `packages.SelectMany(p => p.Templates)`).
|
||||
// FullPath needs to be non-null so the DistinctPackagePathComparer (used to de-dup
|
||||
// ExtraPackages in FindTemplates) doesn't NRE in GetHashCode; using a sentinel path
|
||||
// ensures it doesn't collide with any real package.
|
||||
var synthetic = new Package { FullPath = new UFile("Stride.DotNetNewTemplates.synthetic") };
|
||||
foreach (var template in templates)
|
||||
{
|
||||
// Item templates (asset packs) are not stand-alone projects — they surface as
|
||||
// checkboxes inside the parameter dialog of templates that offer them, not as
|
||||
// entries in the New-Project list.
|
||||
if (IsItemTemplate(template))
|
||||
continue;
|
||||
|
||||
// Cross-ref by template.json identity → matches the sdtpl Id (we set
|
||||
// `identity = sdtpl.Id` in the preprocessor) so the dict lookup hits.
|
||||
sdtplsByIdentity.TryGetValue(template.Identity, out var source);
|
||||
var sdtpl = source?.Metadata;
|
||||
var shortName = template.ShortNameList.FirstOrDefault();
|
||||
// Per-template content dir inside the package, e.g. <packageDir>/content/stride-fps/.
|
||||
// Icon/Screenshot relative paths in .sdtpl resolve against this dir at runtime.
|
||||
var templateContentDir = source != null && shortName != null
|
||||
? Path.Combine(source.InstallSource, "content", shortName)
|
||||
: null;
|
||||
var description = new TemplateDotNetNewDescription
|
||||
if (!InstallBundledPackage(packageId, sdtplsByIdentity, logger)
|
||||
&& ContentTemplateResolver.IsContentPackage(packageId) && packageId != AssetPacksPackageId)
|
||||
{
|
||||
Id = sdtpl?.Id ?? TryParseGuid(template.Identity),
|
||||
Name = sdtpl?.Name ?? template.Name ?? shortName ?? template.Identity,
|
||||
Description = sdtpl?.Description ?? template.Description,
|
||||
FullDescription = sdtpl?.FullDescription,
|
||||
DefaultOutputName = sdtpl?.DefaultOutputName ?? template.DefaultName ?? "MyGame",
|
||||
Group = sdtpl?.Group ?? template.GroupIdentity ?? "Stride",
|
||||
Scope = (sdtpl?.Scope != null && Enum.TryParse<TemplateScope>(sdtpl.Scope, ignoreCase: true, out var parsedScope))
|
||||
? parsedScope
|
||||
: TemplateScope.Session,
|
||||
TemplateIdentity = template.Identity,
|
||||
TemplateShortName = shortName ?? string.Empty,
|
||||
OffersAssetPacks = sdtpl?.HasParameter("assetPacks") == true,
|
||||
// FullPath must be non-null: TemplateDescriptionViewModel calls
|
||||
// Template.FullPath.GetFullDirectory() unconditionally when constructing
|
||||
// image paths. We point it at a synthetic file inside the per-template
|
||||
// content dir so the directory part is the natural relative-resolution root
|
||||
// (matches what TemplateDescription.FullPath means for real .sdpkg-backed
|
||||
// templates). Falls back to a per-identity sentinel if we couldn't resolve
|
||||
// a content dir (e.g. template not in the aggregated metadata).
|
||||
FullPath = templateContentDir != null
|
||||
? new UFile(Path.Combine(templateContentDir, ".synthetic.sdtpl"))
|
||||
: new UFile($"{template.Identity}.synthetic"),
|
||||
Icon = templateContentDir != null ? ResolveTemplateAsset(sdtpl?.Icon, templateContentDir) : null,
|
||||
};
|
||||
if (templateContentDir != null && sdtpl != null)
|
||||
{
|
||||
foreach (var s in sdtpl.Screenshots)
|
||||
{
|
||||
var resolved = ResolveTemplateAsset(s, templateContentDir);
|
||||
if (resolved != null)
|
||||
description.Screenshots.Add(resolved);
|
||||
}
|
||||
missingContent.Add(packageId);
|
||||
}
|
||||
synthetic.Templates.Add(description);
|
||||
}
|
||||
|
||||
TemplateManager.RegisterPackage(synthetic);
|
||||
RegisterNewTemplates(sdtplsByIdentity, logger);
|
||||
}
|
||||
|
||||
if (missingContent.Count > 0)
|
||||
_ = Task.Run(() => DownloadMissingContentAsync(missingContent, logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads content packages (<paramref name="packageIds"/>) this machine doesn't have, then installs them into the
|
||||
/// registry and registers their templates, so they show in the New-Project dialog once ready. Best effort.
|
||||
/// </summary>
|
||||
private static async Task DownloadMissingContentAsync(IReadOnlyList<string> packageIds, Logger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fetched = new List<string>();
|
||||
for (var i = 0; i < packageIds.Count; i++)
|
||||
{
|
||||
if (await InstallContentPackageAsync(packageIds[i], logger, i + 1, packageIds.Count).ConfigureAwait(false) is not null)
|
||||
fetched.Add(packageIds[i]);
|
||||
}
|
||||
if (fetched.Count == 0)
|
||||
return;
|
||||
|
||||
lock (InitLock)
|
||||
{
|
||||
var sdtplsByIdentity = new Dictionary<string, TemplateMetadataSource>();
|
||||
foreach (var packageId in fetched)
|
||||
InstallBundledPackage(packageId, sdtplsByIdentity, logger);
|
||||
RegisterNewTemplates(sdtplsByIdentity, logger);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Warning($"Could not download the template content ({string.Join(", ", packageIds)}): {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps every registry template not registered yet as a <see cref="TemplateDotNetNewDescription"/>, in one
|
||||
/// synthetic package registered with <see cref="TemplateManager"/>. Caller holds <see cref="InitLock"/>.
|
||||
/// </summary>
|
||||
private static void RegisterNewTemplates(Dictionary<string, TemplateMetadataSource> sdtplsByIdentity, Logger logger)
|
||||
{
|
||||
var templatesTask = registry!.GetTemplatesAsync();
|
||||
templatesTask.Wait();
|
||||
var templates = templatesTask.Result;
|
||||
logger.Info($"Loaded {templates.Count} dotnet new template(s) total");
|
||||
|
||||
var added = 0;
|
||||
foreach (var template in templates)
|
||||
{
|
||||
// Item templates (asset packs) are not stand-alone projects — they surface as
|
||||
// checkboxes inside the parameter dialog of templates that offer them, not as
|
||||
// entries in the New-Project list.
|
||||
if (IsItemTemplate(template))
|
||||
continue;
|
||||
|
||||
// Registered by an earlier pass (startup, then the background download).
|
||||
if (!RegisteredTemplateIdentities.Add(template.Identity))
|
||||
continue;
|
||||
|
||||
// Cross-ref by template.json identity → matches the sdtpl Id (we set
|
||||
// `identity = sdtpl.Id` in the preprocessor) so the dict lookup hits.
|
||||
sdtplsByIdentity.TryGetValue(template.Identity, out var source);
|
||||
var sdtpl = source?.Metadata;
|
||||
var shortName = template.ShortNameList.FirstOrDefault();
|
||||
// Per-template content dir inside the package, e.g. <packageDir>/content/stride-fps/.
|
||||
// Icon/Screenshot relative paths in .sdtpl resolve against this dir at runtime.
|
||||
var templateContentDir = source != null && shortName != null
|
||||
? Path.Combine(source.InstallSource, "content", shortName)
|
||||
: null;
|
||||
var description = new TemplateDotNetNewDescription
|
||||
{
|
||||
Id = sdtpl?.Id ?? TryParseGuid(template.Identity),
|
||||
Name = sdtpl?.Name ?? template.Name ?? shortName ?? template.Identity,
|
||||
Description = sdtpl?.Description ?? template.Description,
|
||||
FullDescription = sdtpl?.FullDescription,
|
||||
DefaultOutputName = sdtpl?.DefaultOutputName ?? template.DefaultName ?? "MyGame",
|
||||
Group = sdtpl?.Group ?? template.GroupIdentity ?? "Stride",
|
||||
Scope = (sdtpl?.Scope != null && Enum.TryParse<TemplateScope>(sdtpl.Scope, ignoreCase: true, out var parsedScope))
|
||||
? parsedScope
|
||||
: TemplateScope.Session,
|
||||
TemplateIdentity = template.Identity,
|
||||
TemplateShortName = shortName ?? string.Empty,
|
||||
OffersAssetPacks = sdtpl?.HasParameter("assetPacks") == true,
|
||||
// FullPath must be non-null: TemplateDescriptionViewModel calls
|
||||
// Template.FullPath.GetFullDirectory() unconditionally when constructing
|
||||
// image paths. We point it at a synthetic file inside the per-template
|
||||
// content dir so the directory part is the natural relative-resolution root
|
||||
// (matches what TemplateDescription.FullPath means for real .sdpkg-backed
|
||||
// templates). Falls back to a per-identity sentinel if we couldn't resolve
|
||||
// a content dir (e.g. template not in the aggregated metadata).
|
||||
FullPath = templateContentDir != null
|
||||
? new UFile(Path.Combine(templateContentDir, ".synthetic.sdtpl"))
|
||||
: new UFile($"{template.Identity}.synthetic"),
|
||||
Icon = templateContentDir != null ? ResolveTemplateAsset(sdtpl?.Icon, templateContentDir) : null,
|
||||
};
|
||||
if (templateContentDir != null && sdtpl != null)
|
||||
{
|
||||
foreach (var s in sdtpl.Screenshots)
|
||||
{
|
||||
var resolved = ResolveTemplateAsset(s, templateContentDir);
|
||||
if (resolved != null)
|
||||
description.Screenshots.Add(resolved);
|
||||
}
|
||||
}
|
||||
SyntheticPackage.Templates.Add(description);
|
||||
added++;
|
||||
}
|
||||
|
||||
// Registering the same package again signals the new descriptions to an open template list.
|
||||
if (added > 0)
|
||||
TemplateManager.RegisterPackage(SyntheticPackage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The asset-pack item templates available for the New Game flow, in registry order. When
|
||||
/// none are installed and <paramref name="allowDownload"/> is true, resolves the
|
||||
/// <see cref="AssetPacksPackageId"/> package via <see cref="PackageStore"/> — downloading it
|
||||
/// from the configured NuGet sources if this machine doesn't have it — and installs it into
|
||||
/// <see cref="AssetPacksPackageId"/> package at this engine's content version — downloading
|
||||
/// it from the configured NuGet sources if this machine doesn't have it — and installs it into
|
||||
/// the registry. Returns an empty list when the package can't be obtained (e.g. offline);
|
||||
/// callers degrade by not offering packs.
|
||||
/// </summary>
|
||||
@@ -195,28 +262,21 @@ public static class DotNetNewTemplateBridge
|
||||
if (packs.Count > 0 || !allowDownload)
|
||||
return packs;
|
||||
|
||||
var version = new PackageVersion(DesiredVersionFor(AssetPacksPackageId));
|
||||
var versionRange = new PackageVersionRange(version, true, version, true);
|
||||
if (PackageStore.Instance.GetPackageDirectory(AssetPacksPackageId, versionRange) is null)
|
||||
{
|
||||
logger.Info($"{AssetPacksPackageId} {version} not present locally; downloading...");
|
||||
try
|
||||
// Download first, then the same path as startup probing: install the now-local package into the registry
|
||||
// (no TemplateManager registration — item templates stay out of the New-Project list). InstallBundledPackage
|
||||
// is synchronous and reindexes the package, so it goes to the thread pool rather than the UI thread.
|
||||
var installed = await InstallContentPackageAsync(AssetPacksPackageId, logger).ConfigureAwait(false) is not null
|
||||
&& await Task.Run(() =>
|
||||
{
|
||||
await PackageStore.Instance.InstallPackage(AssetPacksPackageId, version).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Warning($"Could not download {AssetPacksPackageId} {version}: {e.Message}. Asset packs will be unavailable.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
lock (InitLock)
|
||||
lock (InitLock)
|
||||
{
|
||||
return InstallBundledPackage(AssetPacksPackageId, new Dictionary<string, TemplateMetadataSource>(), logger);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
if (!installed)
|
||||
{
|
||||
// Same path as startup probing: resolves the package dir and installs it into the
|
||||
// registry (no TemplateManager registration — item templates stay out of the
|
||||
// New-Project list).
|
||||
InstallBundledPackage(AssetPacksPackageId, new Dictionary<string, TemplateMetadataSource>(), logger);
|
||||
logger.Warning($"{AssetPacksPackageId} is unavailable; asset packs will not be offered.");
|
||||
return [];
|
||||
}
|
||||
return FilterAssetPacks(await registry.GetTemplatesAsync().ConfigureAwait(false));
|
||||
}
|
||||
@@ -230,16 +290,95 @@ public static class DotNetNewTemplateBridge
|
||||
=> template.TagsCollection.TryGetValue("type", out var type) && string.Equals(type, "item", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The package version this build expects for <paramref name="packageId"/>: content packages
|
||||
/// (Samples, Starters, AssetPacks) use <see cref="StrideVersion.SamplesVersion"/> + the engine NuGet suffix;
|
||||
/// everything else tracks <see cref="StrideVersion.NuGetVersion"/>.
|
||||
/// The content version this engine names (Samples, Starters, AssetPacks): the committed value of
|
||||
/// sources/templates/StrideSamplesVersion.props, embedded as a resource.
|
||||
/// </summary>
|
||||
private static string DesiredVersionFor(string packageId)
|
||||
private static PackageVersion ContentVersion => LazyContentVersion.Value;
|
||||
|
||||
private static readonly Lazy<PackageVersion> LazyContentVersion = new(() =>
|
||||
{
|
||||
var contentVersioned = packageId is "Stride.Templates.Samples" or "Stride.Templates.Games.Starters" or AssetPacksPackageId;
|
||||
return contentVersioned
|
||||
? StrideVersion.SamplesVersion + StrideVersion.NuGetVersionSuffix
|
||||
: StrideVersion.NuGetVersion;
|
||||
using var stream = typeof(DotNetNewTemplateBridge).Assembly
|
||||
.GetManifestResourceStream("StrideSamplesVersion.props")
|
||||
?? throw new InvalidOperationException("StrideSamplesVersion.props is not embedded in Stride.Assets.");
|
||||
using var reader = new StreamReader(stream);
|
||||
var match = Regex.Match(reader.ReadToEnd(), "<_StrideCommittedSamplesVersion>([^<]+)</");
|
||||
return match.Success
|
||||
? new PackageVersion(match.Groups[1].Value)
|
||||
: throw new InvalidOperationException("No content version in the embedded StrideSamplesVersion.props.");
|
||||
});
|
||||
|
||||
/// <summary>This build's own version, the engine the content is resolved for.</summary>
|
||||
private static PackageVersion HostVersion => new(StrideVersion.NuGetVersion);
|
||||
|
||||
/// <summary>
|
||||
/// The installed directory of <paramref name="packageId"/> for this build, null when it is not installed. Never
|
||||
/// downloads (<see cref="InstallContentPackageAsync"/> does): this also runs on the UI thread at startup. Content
|
||||
/// packages resolve through <see cref="ContentTemplateResolver"/> (this checkout's own pack of the content
|
||||
/// version, or exactly the content version); everything else is exactly <see cref="StrideVersion.NuGetVersion"/>.
|
||||
/// </summary>
|
||||
private static UDirectory? ResolvePackageDirectory(string packageId, Logger logger)
|
||||
{
|
||||
if (ContentTemplateResolver.IsContentPackage(packageId))
|
||||
{
|
||||
var installed = PackageStore.Instance.GetLocalPackages(packageId, ContentTemplateResolver.LocalBuildRange(ContentVersion, HostVersion));
|
||||
var package = ContentTemplateResolver.Pick(installed, ContentVersion, HostVersion);
|
||||
logger.Info(package is null
|
||||
? $"{packageId}: content version {ContentVersion} is not installed."
|
||||
: $"{packageId}: using {package.Version} (content version {ContentVersion}).");
|
||||
return package is not null ? PackageStore.Instance.GetPackageDirectory(package) : null;
|
||||
}
|
||||
|
||||
// Exact range, so release/prerelease ordering is moot.
|
||||
var exact = new PackageVersionRange(HostVersion, true, HostVersion, true);
|
||||
return PackageStore.Instance.GetPackageDirectory(packageId, exact);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves content package <paramref name="packageId"/>, downloading it when missing, and reports the download to
|
||||
/// <see cref="TemplateDownloads"/> so the template windows can show it.
|
||||
/// </summary>
|
||||
/// <param name="index">Position of <paramref name="packageId"/> in its batch, from 1 (shown with the progress).</param>
|
||||
/// <param name="count">Number of packages in the batch.</param>
|
||||
private static async Task<NugetLocalPackage?> InstallContentPackageAsync(string packageId, Logger logger, int index = 1, int count = 1)
|
||||
{
|
||||
// One content download at a time: the store reports progress for all of them together.
|
||||
await DownloadGate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var downloading = false;
|
||||
// A content install downloads the package and at most its small engine marker dependency, both started
|
||||
// together, so the size of the started downloads is the size to show.
|
||||
void OnProgress(long downloadedBytes)
|
||||
=> TemplateDownloads.Report(new TemplateDownload(packageId, downloadedBytes, PackageStore.Instance.StartedDownloadBytes, index, count));
|
||||
Task<NugetLocalPackage?> Install(string id, PackageVersion version)
|
||||
{
|
||||
downloading = true;
|
||||
TemplateDownloads.Report(new TemplateDownload(packageId, 0, 0, index, count));
|
||||
return PackageStore.Instance.InstallPackage(id, version);
|
||||
}
|
||||
|
||||
PackageStore.Instance.DownloadProgress += OnProgress;
|
||||
NugetLocalPackage? package = null;
|
||||
try
|
||||
{
|
||||
package = await ContentTemplateResolver.ResolveAsync(
|
||||
packageId, ContentVersion, HostVersion,
|
||||
PackageStore.Instance.GetLocalPackages,
|
||||
Install,
|
||||
message => logger.Info(message)).ConfigureAwait(false);
|
||||
return package;
|
||||
}
|
||||
finally
|
||||
{
|
||||
PackageStore.Instance.DownloadProgress -= OnProgress;
|
||||
if (downloading)
|
||||
TemplateDownloads.Report(package is null ? new TemplateDownload(packageId, 0, 0, index, count, Failed: true) : null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
DownloadGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -291,8 +430,8 @@ public static class DotNetNewTemplateBridge
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="mountPointUri"/> exists AND — for packages we manage — its version
|
||||
/// matches <see cref="DesiredVersionFor"/>. Mount points use NuGet's global-folder layout
|
||||
/// True when <paramref name="mountPointUri"/> exists AND — for packages we manage — its version is one this
|
||||
/// build may use (see <see cref="ResolvePackageDirectory"/>). Mount points use NuGet's global-folder layout
|
||||
/// (<c><root>\<id>\<version></c>), so id/version are the trailing two segments;
|
||||
/// unrecognized entries are left untouched.
|
||||
/// </summary>
|
||||
@@ -302,43 +441,36 @@ public static class DotNetNewTemplateBridge
|
||||
return false;
|
||||
|
||||
var trimmed = mountPointUri.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var version = Path.GetFileName(trimmed);
|
||||
var versionText = Path.GetFileName(trimmed);
|
||||
var id = Path.GetFileName(Path.GetDirectoryName(trimmed) ?? string.Empty);
|
||||
|
||||
var managedId = BundledTemplatePackageIds.FirstOrDefault(p => string.Equals(p, id, StringComparison.OrdinalIgnoreCase));
|
||||
if (managedId is null)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
return new PackageVersion(version).Equals(new PackageVersion(DesiredVersionFor(managedId)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Equals(version, DesiredVersionFor(managedId), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
if (!PackageVersion.TryParse(versionText, out var version))
|
||||
return false;
|
||||
return ContentTemplateResolver.IsContentPackage(managedId)
|
||||
? ContentTemplateResolver.IsAcceptable(version, ContentVersion, HostVersion)
|
||||
: version.Equals(HostVersion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <paramref name="packageId"/> via <see cref="PackageStore"/>, installs it into the
|
||||
/// registry (skipping if unchanged since last session), and merges its <c>templates.sdtpls</c>
|
||||
/// entries into <paramref name="sdtplsByIdentity"/>. Tolerates a missing package by logging.
|
||||
/// Resolves <paramref name="packageId"/> for this build (<see cref="ResolvePackageDirectory"/>), installs it
|
||||
/// into the registry (skipping if unchanged since last session), and merges its <c>templates.sdtpls</c>
|
||||
/// entries into <paramref name="sdtplsByIdentity"/>. Tolerates a missing package by logging; returns whether
|
||||
/// the package is installed in the registry.
|
||||
/// </summary>
|
||||
private static void InstallBundledPackage(string packageId, Dictionary<string, TemplateMetadataSource> sdtplsByIdentity, Logger logger)
|
||||
private static bool InstallBundledPackage(string packageId, Dictionary<string, TemplateMetadataSource> sdtplsByIdentity, Logger logger)
|
||||
{
|
||||
// Resolve each package at the exact version it's packed at (exact range, so release/prerelease ordering is
|
||||
// moot). Content-versioned (Samples, Starters) = StrideSamplesVersion + engine suffix, matching the pack in
|
||||
// Stride.Templates.Common.targets from the same source; Games is engine-versioned.
|
||||
var version = new PackageVersion(DesiredVersionFor(packageId));
|
||||
var versionRange = new PackageVersionRange(version, true, version, true);
|
||||
var packageDir = PackageStore.Instance.GetPackageDirectory(packageId, versionRange);
|
||||
var packageDir = ResolvePackageDirectory(packageId, logger);
|
||||
if (packageDir is null)
|
||||
{
|
||||
// Not installed yet (e.g. fresh checkout before first build of that templates
|
||||
// project). Not fatal — the editor still opens; this package's templates just
|
||||
// won't appear in the New-Project dialog until built at least once.
|
||||
logger.Warning($"{packageId} {version} not found via PackageStore; its templates will be unavailable.");
|
||||
return;
|
||||
// Not installed and not obtainable (a fresh checkout before the first build of Stride.Templates.Games,
|
||||
// or content that is not published yet / no package source reachable). Not fatal — the editor still
|
||||
// opens; this package's templates just won't appear in the New-Project dialog.
|
||||
logger.Warning($"{packageId} is not available; its templates will be unavailable.");
|
||||
return false;
|
||||
}
|
||||
// Install from the extracted package directory rather than the .nupkg file inside it.
|
||||
// Reasoning: pointing the bootstrapper at the .nupkg path inside NuGet's global cache
|
||||
@@ -371,7 +503,7 @@ public static class DotNetNewTemplateBridge
|
||||
{
|
||||
foreach (var d in diagnostics)
|
||||
logger.Error($"Template install ({packageId}): {d}");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +511,7 @@ public static class DotNetNewTemplateBridge
|
||||
// the installSource it came from so Icon/Screenshot relative paths can be resolved later.
|
||||
foreach (var (id, meta) in LoadAggregatedSdtpls(installSource, logger))
|
||||
sdtplsByIdentity[id] = new TemplateMetadataSource(meta, installSource);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
using Microsoft.TemplateEngine.Edge.Template;
|
||||
@@ -110,7 +111,9 @@ public class DotNetNewTemplateGenerator : SessionTemplateGenerator
|
||||
if (sdpkg == null)
|
||||
return false;
|
||||
InstantiateAssetPacks(parameters, sdpkg);
|
||||
if (!UpgradeGeneratedProjects(parameters))
|
||||
// Content packed for an older engine keeps its version here: IntegrateIntoSession runs the package
|
||||
// upgraders on it, which migrate the assets and the code and stamp the version themselves.
|
||||
if (!UpgradeGeneratedProjects(parameters, stampOlderContent: false))
|
||||
return false;
|
||||
return IntegrateIntoSession(sdpkg, parameters);
|
||||
}
|
||||
@@ -244,16 +247,46 @@ public class DotNetNewTemplateGenerator : SessionTemplateGenerator
|
||||
/// them to <see cref="StridePackageUpgrader.CurrentVersion"/>. Runs without a session or
|
||||
/// AssemblyContainer — same standalone path the legacy session-load uses internally.
|
||||
/// </summary>
|
||||
protected virtual bool UpgradeGeneratedProjects(SessionTemplateGeneratorParameters parameters)
|
||||
/// <param name="stampOlderContent">
|
||||
/// Whether content packed for an older engine is stamped too. False when a session load follows: the package
|
||||
/// upgraders it runs need the version the content came from, and stamping would leave the content looking
|
||||
/// current and never upgraded.
|
||||
/// </param>
|
||||
protected virtual bool UpgradeGeneratedProjects(SessionTemplateGeneratorParameters parameters, bool stampOlderContent = true)
|
||||
{
|
||||
var log = parameters.Logger;
|
||||
foreach (var csproj in Directory.EnumerateFiles(parameters.OutputDirectory, "*.csproj", SearchOption.AllDirectories))
|
||||
var csprojs = Directory.EnumerateFiles(parameters.OutputDirectory, "*.csproj", SearchOption.AllDirectories).ToList();
|
||||
|
||||
if (!stampOlderContent && GetContentEngineVersion(csprojs) is { } contentVersion
|
||||
&& contentVersion.Version < new PackageVersion(StridePackageUpgrader.CurrentVersion).Version)
|
||||
{
|
||||
log.Info($"Template content is written for Stride {contentVersion}; it is upgraded to {StridePackageUpgrader.CurrentVersion} as it is loaded.");
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var csproj in csprojs)
|
||||
{
|
||||
StridePackageUpgrader.UpgradeProjectVersions(csproj, log);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Stride.Engine version the instantiated content names, null when none of its projects references it
|
||||
/// (a template of assets alone). The prerelease suffix is part of it; upgrades compare the numbers only.
|
||||
/// </summary>
|
||||
private static PackageVersion GetContentEngineVersion(IEnumerable<string> csprojPaths)
|
||||
{
|
||||
foreach (var csprojPath in csprojPaths)
|
||||
{
|
||||
var match = Regex.Match(File.ReadAllText(csprojPath), "Stride\\.Engine\"\\s+Version=\"([^\"]+)\"");
|
||||
if (match.Success && PackageVersion.TryParse(match.Groups[1].Value, out var version))
|
||||
return version;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3 — wires the generated package into <see cref="TemplateGeneratorParameters.Session"/>:
|
||||
/// <see cref="PackageSession.AddExistingProject"/>, per-platform exec registration,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
@@ -47,6 +48,7 @@ public sealed class DotNetNewTemplateRegistry : IDisposable
|
||||
// IPathInfo directly (only EngineEnvironmentSettings does); redirecting via IEnvironment
|
||||
// is the cleanest hook into DefaultPathInfo's path derivation.
|
||||
// "dotnetcli" fallback so our host reads each template's dotnetcli.host.json (isVisible etc.).
|
||||
RemoveDuplicateEntries(profileDir);
|
||||
var host = new DefaultTemplateEngineHost(HostIdentifier, hostVersion, fallbackHostTemplateConfigNames: new[] { "dotnetcli" });
|
||||
var environment = new StrideTemplateEngineEnvironment(profileDir);
|
||||
bootstrapper = new Bootstrapper(
|
||||
@@ -56,6 +58,37 @@ public sealed class DotNetNewTemplateRegistry : IDisposable
|
||||
environment: environment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps one entry per mount point in the persisted <c>packages.json</c> (the last one, the latest install): a
|
||||
/// forced reinstall of a <c>.nupkg</c> adds an entry each time.
|
||||
/// </summary>
|
||||
private static void RemoveDuplicateEntries(string profileDir)
|
||||
{
|
||||
var packagesJson = Path.Combine(profileDir, ".templateengine", "packages.json");
|
||||
try
|
||||
{
|
||||
if (!File.Exists(packagesJson) || JsonNode.Parse(File.ReadAllText(packagesJson))?["Packages"] is not JsonArray packages)
|
||||
return;
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var removed = 0;
|
||||
for (var i = packages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var uri = packages[i]?["MountPointUri"]?.GetValue<string>();
|
||||
if (uri is null || seen.Add(uri))
|
||||
continue;
|
||||
packages.RemoveAt(i);
|
||||
removed++;
|
||||
}
|
||||
if (removed > 0)
|
||||
File.WriteAllText(packagesJson, packages.Root.ToJsonString());
|
||||
}
|
||||
catch (Exception e) when (e is IOException or UnauthorizedAccessException or System.Text.Json.JsonException or InvalidOperationException)
|
||||
{
|
||||
// Best effort: the template engine reads the file as it is.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs (or reinstalls) the template package at <paramref name="source"/>. Accepts either
|
||||
/// a path to a <c>.nupkg</c> file (handled by the NuGet installer) or a path to an extracted
|
||||
@@ -88,6 +121,24 @@ public sealed class DotNetNewTemplateRegistry : IDisposable
|
||||
return (success, diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls the installed packages that none of <paramref name="sources"/> (as given to
|
||||
/// <see cref="InstallPackageAsync"/>) installed, so templates of a package version no longer resolved do not
|
||||
/// linger next to the current ones. A <c>.nupkg</c> source is matched by file name, since the registry mounts
|
||||
/// its own copy.
|
||||
/// </summary>
|
||||
public async Task RemovePackagesExceptAsync(IReadOnlyCollection<string> sources, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var directories = new HashSet<string>(sources.Where(Directory.Exists).Select(s => Path.GetFullPath(s).TrimEnd('\\', '/')), StringComparer.OrdinalIgnoreCase);
|
||||
var nupkgNames = new HashSet<string>(sources.Where(File.Exists).Select(Path.GetFileName)!, StringComparer.OrdinalIgnoreCase);
|
||||
var stale = (await bootstrapper.GetManagedTemplatePackagesAsync(cancellationToken).ConfigureAwait(false))
|
||||
.Where(package => !directories.Contains(package.MountPointUri.TrimEnd('\\', '/'))
|
||||
&& !nupkgNames.Contains(Path.GetFileName(package.MountPointUri)))
|
||||
.ToList();
|
||||
if (stale.Count > 0)
|
||||
await bootstrapper.UninstallTemplatePackagesAsync(stale, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Enumerates all available templates (across all installed packages).</summary>
|
||||
public async Task<IReadOnlyList<ITemplateInfo>> GetTemplatesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
namespace Stride.Graphics.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a structured buffer written by a previous dispatch and increments each element.
|
||||
/// </summary>
|
||||
internal shader BufferBarrierReadTestShader : ComputeShaderBase
|
||||
{
|
||||
stage StructuredBuffer<uint> Input;
|
||||
stage RWStructuredBuffer<uint> Result;
|
||||
|
||||
override void Compute()
|
||||
{
|
||||
Result[streams.DispatchThreadId.x] = Input[streams.DispatchThreadId.x] + 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
namespace Stride.Graphics.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a value derived from each thread's dispatch index into a structured buffer.
|
||||
/// </summary>
|
||||
internal shader BufferBarrierTestShader : ComputeShaderBase
|
||||
{
|
||||
stage RWStructuredBuffer<uint> Output;
|
||||
|
||||
override void Compute()
|
||||
{
|
||||
Output[streams.DispatchThreadId.x] = streams.DispatchThreadId.x * 3;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Xunit;
|
||||
|
||||
using Stride.Core;
|
||||
using Stride.Core.Mathematics;
|
||||
using Stride.Rendering;
|
||||
using Stride.Rendering.ComputeEffect;
|
||||
|
||||
namespace Stride.Graphics.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Covers a compute write handed to a second dispatch that reads it as a shader resource.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The consumer is a dispatch rather than a readback: a readback copy emits its own barrier from
|
||||
/// the buffer's access and stage masks, which would synchronise the write even with no transition
|
||||
/// and leave the test unable to fail. Run with STRIDE_VULKAN_SYNC_VALIDATION=1 to have Vulkan
|
||||
/// report the hazard when the transition is missing.
|
||||
/// </remarks>
|
||||
public class TestBufferBarrier : GraphicTestGameBase
|
||||
{
|
||||
private const int ElementCount = 64;
|
||||
|
||||
private ComputeEffectShader writeEffect;
|
||||
private ComputeEffectShader readEffect;
|
||||
private Buffer sharedBuffer;
|
||||
private Buffer resultBuffer;
|
||||
|
||||
protected override void RegisterTests()
|
||||
{
|
||||
base.RegisterTests();
|
||||
|
||||
FrameGameSystem.Draw(ComputeWriteIsVisibleToASubsequentDispatch);
|
||||
}
|
||||
|
||||
protected override async Task LoadContent()
|
||||
{
|
||||
await base.LoadContent();
|
||||
|
||||
sharedBuffer = Buffer.Structured.New<uint>(GraphicsDevice, ElementCount, unorderedAccess: true).DisposeBy(this);
|
||||
resultBuffer = Buffer.Structured.New<uint>(GraphicsDevice, ElementCount, unorderedAccess: true).DisposeBy(this);
|
||||
|
||||
var renderContext = RenderContext.GetShared(Services);
|
||||
writeEffect = new ComputeEffectShader(renderContext)
|
||||
{
|
||||
ShaderSourceName = "BufferBarrierTestShader",
|
||||
ThreadNumbers = new Int3(ElementCount, 1, 1),
|
||||
ThreadGroupCounts = new Int3(1, 1, 1),
|
||||
};
|
||||
writeEffect.DisposeBy(this);
|
||||
|
||||
readEffect = new ComputeEffectShader(renderContext)
|
||||
{
|
||||
ShaderSourceName = "BufferBarrierReadTestShader",
|
||||
ThreadNumbers = new Int3(ElementCount, 1, 1),
|
||||
ThreadGroupCounts = new Int3(1, 1, 1),
|
||||
};
|
||||
readEffect.DisposeBy(this);
|
||||
}
|
||||
|
||||
private void ComputeWriteIsVisibleToASubsequentDispatch()
|
||||
{
|
||||
var commandList = GraphicsContext.CommandList;
|
||||
var renderDrawContext = new RenderDrawContext(Services, RenderContext.GetShared(Services), GraphicsContext);
|
||||
|
||||
commandList.ResourceBarrierTransition(sharedBuffer, BarrierLayout.UnorderedAccess);
|
||||
writeEffect.Parameters.Set(BufferBarrierTestShaderKeys.Output, sharedBuffer);
|
||||
((RendererBase)writeEffect).Draw(renderDrawContext);
|
||||
|
||||
commandList.ResourceBarrierTransition(sharedBuffer, BarrierLayout.ShaderResource);
|
||||
commandList.ResourceBarrierTransition(resultBuffer, BarrierLayout.UnorderedAccess);
|
||||
readEffect.Parameters.Set(BufferBarrierReadTestShaderKeys.Input, sharedBuffer);
|
||||
readEffect.Parameters.Set(BufferBarrierReadTestShaderKeys.Result, resultBuffer);
|
||||
((RendererBase)readEffect).Draw(renderDrawContext);
|
||||
|
||||
var values = resultBuffer.GetData<uint>(commandList);
|
||||
|
||||
Assert.Equal(ElementCount, values.Length);
|
||||
for (uint i = 0; i < ElementCount; i++)
|
||||
{
|
||||
Assert.Equal((i * 3) + 1, values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void ComputeWriteIsVisibleToASubsequentRead()
|
||||
{
|
||||
RunGameTest(new TestBufferBarrier());
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,14 @@ public enum BarrierLayout
|
||||
Common,
|
||||
|
||||
/// <summary>
|
||||
/// The resource is used as a render target.
|
||||
/// The resource is used as a render target. Covers reads as well as writes, because blending
|
||||
/// and a load operation that preserves the existing contents both read the attachment.
|
||||
/// </summary>
|
||||
RenderTarget,
|
||||
|
||||
/// <summary>
|
||||
/// The resource is used as a writable depth-stencil buffer.
|
||||
/// The resource is used as a writable depth-stencil buffer. Covers reads as well as writes,
|
||||
/// because the depth and stencil tests read the attachment.
|
||||
/// </summary>
|
||||
DepthStencilWrite,
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ internal static class BarrierMapping
|
||||
/// </summary>
|
||||
internal static VkAccessFlags ToVkAccessFlags(BarrierLayout layout) => layout switch
|
||||
{
|
||||
BarrierLayout.RenderTarget => VkAccessFlags.ColorAttachmentWrite,
|
||||
BarrierLayout.DepthStencilWrite => VkAccessFlags.DepthStencilAttachmentWrite,
|
||||
BarrierLayout.RenderTarget => VkAccessFlags.ColorAttachmentWrite | VkAccessFlags.ColorAttachmentRead,
|
||||
BarrierLayout.DepthStencilWrite => VkAccessFlags.DepthStencilAttachmentWrite | VkAccessFlags.DepthStencilAttachmentRead,
|
||||
BarrierLayout.DepthStencilRead => VkAccessFlags.DepthStencilAttachmentRead,
|
||||
BarrierLayout.ShaderResource => VkAccessFlags.ShaderRead,
|
||||
BarrierLayout.UnorderedAccess => VkAccessFlags.ShaderRead | VkAccessFlags.ShaderWrite,
|
||||
@@ -75,7 +75,7 @@ internal static class BarrierMapping
|
||||
BarrierLayout.RenderTarget => VkPipelineStageFlags.ColorAttachmentOutput,
|
||||
BarrierLayout.DepthStencilWrite => VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests | VkPipelineStageFlags.LateFragmentTests,
|
||||
BarrierLayout.DepthStencilRead => VkPipelineStageFlags.EarlyFragmentTests | VkPipelineStageFlags.LateFragmentTests,
|
||||
BarrierLayout.ShaderResource => VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader,
|
||||
BarrierLayout.ShaderResource => VkPipelineStageFlags.VertexShader | VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader,
|
||||
BarrierLayout.UnorderedAccess => VkPipelineStageFlags.ComputeShader,
|
||||
BarrierLayout.CopySource => VkPipelineStageFlags.Transfer,
|
||||
BarrierLayout.CopyDest => VkPipelineStageFlags.Transfer,
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace Stride.Graphics
|
||||
// that's a known limitation to be addressed by adding a "last-submitted layout" tracker.
|
||||
private readonly Dictionary<Texture, BarrierLayout> currentCbLayouts = new();
|
||||
|
||||
/// <summary>
|
||||
/// The last access this command buffer synchronised each buffer against.
|
||||
/// </summary>
|
||||
private readonly Dictionary<Buffer, BarrierLayout> currentCbBufferLayouts = new();
|
||||
|
||||
// Dynamic rendering state: whether a render pass instance (vkCmdBeginRendering) is active,
|
||||
// whether the bound attachments changed since it began, and which depth layout it declared.
|
||||
private bool activeRendering;
|
||||
@@ -85,6 +90,7 @@ namespace Stride.Graphics
|
||||
CleanupRenderPass();
|
||||
boundDescriptorSets.Clear();
|
||||
currentCbLayouts.Clear();
|
||||
currentCbBufferLayouts.Clear();
|
||||
|
||||
renderingDirty = true;
|
||||
|
||||
@@ -487,6 +493,11 @@ namespace Stride.Graphics
|
||||
/// <summary>
|
||||
/// Transitions a resource to a new layout using the cross-platform barrier abstraction.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a <see cref="Buffer"/>, <see cref="Buffer.NativeAccessMask"/> and
|
||||
/// <see cref="Buffer.NativePipelineStageMask"/> are read as the source but never written.
|
||||
/// The copy and upload paths rely on them staying the set of every legal usage.
|
||||
/// </remarks>
|
||||
// TODO: subresource parameter is currently ignored — Vulkan always transitions all
|
||||
// subresources. This is incorrect when different subresources need different layouts
|
||||
// (e.g. reading mip 0 as ShaderResource while writing mip 1 as RenderTarget).
|
||||
@@ -543,12 +554,64 @@ namespace Stride.Graphics
|
||||
var memoryBarrier = new VkImageMemoryBarrier(texture.NativeImage, new VkImageSubresourceRange(texture.NativeImageAspect, 0, uint.MaxValue, 0, uint.MaxValue), oldAccessMask, newAccessMask, oldLayout, newVkLayout);
|
||||
GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(currentCommandList.NativeCommandBuffer, sourceStages, newStages, VkDependencyFlags.None, 0, null, 0, null, 1, &memoryBarrier);
|
||||
}
|
||||
else if (resource is Buffer buffer)
|
||||
{
|
||||
VkAccessFlags oldAccessMask;
|
||||
VkPipelineStageFlags sourceStages;
|
||||
if (currentCbBufferLayouts.TryGetValue(buffer, out var fromLayout))
|
||||
{
|
||||
if (fromLayout == newLayout)
|
||||
return;
|
||||
|
||||
oldAccessMask = BarrierMapping.ToVkAccessFlags(fromLayout);
|
||||
sourceStages = BarrierMapping.ToVkPipelineStageFlags(fromLayout);
|
||||
}
|
||||
else
|
||||
{
|
||||
oldAccessMask = buffer.NativeAccessMask;
|
||||
sourceStages = buffer.NativePipelineStageMask;
|
||||
}
|
||||
|
||||
var newAccessMask = BarrierMapping.ToVkAccessFlags(newLayout);
|
||||
var newStages = BarrierMapping.ToVkPipelineStageFlags(newLayout);
|
||||
|
||||
sourceStages = FixStagesForAccess(sourceStages, oldAccessMask);
|
||||
newStages = FixStagesForAccess(newStages, newAccessMask);
|
||||
|
||||
if (sourceStages == VkPipelineStageFlags.None)
|
||||
sourceStages = VkPipelineStageFlags.TopOfPipe;
|
||||
if (newStages == VkPipelineStageFlags.None)
|
||||
newStages = VkPipelineStageFlags.BottomOfPipe;
|
||||
|
||||
currentCbBufferLayouts[buffer] = newLayout;
|
||||
|
||||
CleanupRenderPass();
|
||||
|
||||
var bufferMemoryBarrier = new VkBufferMemoryBarrier(buffer.NativeBuffer, oldAccessMask, newAccessMask);
|
||||
GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(currentCommandList.NativeCommandBuffer, sourceStages, newStages, VkDependencyFlags.None, 0, null, 1, &bufferMemoryBarrier, 0, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adopts the transfer layout as the tracked state of a texture that has no defined layout.
|
||||
/// The restore barrier after a copy then has a legal target.
|
||||
/// </summary>
|
||||
private void AdoptTransferLayoutIfUndefined(Texture texture, BarrierLayout transferLayout)
|
||||
{
|
||||
if (texture.NativeLayout != VkImageLayout.Undefined)
|
||||
return;
|
||||
|
||||
texture.NativeLayout = BarrierMapping.ToVkImageLayout(transferLayout);
|
||||
texture.NativeAccessMask = BarrierMapping.ToVkAccessFlags(transferLayout);
|
||||
texture.NativePipelineStageMask = BarrierMapping.ToVkPipelineStageFlags(transferLayout);
|
||||
texture.LayoutTracker.Set(uint.MaxValue, transferLayout);
|
||||
currentCbLayouts[texture] = transferLayout;
|
||||
}
|
||||
|
||||
[Obsolete("Use BarrierLayout overload instead.")]
|
||||
public void ResourceBarrierTransition(GraphicsResource resource, GraphicsResourceState newState)
|
||||
{
|
||||
@@ -1077,6 +1140,7 @@ namespace Stride.Graphics
|
||||
}
|
||||
else
|
||||
{
|
||||
AdoptTransferLayoutIfUndefined(sourceParent, BarrierLayout.CopySource);
|
||||
imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferSrcOptimal;
|
||||
imageBarriers[imageBarrierCount].newLayout = sourceParent.NativeLayout;
|
||||
imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferRead;
|
||||
@@ -1092,6 +1156,7 @@ namespace Stride.Graphics
|
||||
}
|
||||
else
|
||||
{
|
||||
AdoptTransferLayoutIfUndefined(destinationParent, BarrierLayout.CopyDest);
|
||||
imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferDstOptimal;
|
||||
imageBarriers[imageBarrierCount].newLayout = destinationParent.NativeLayout;
|
||||
imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferWrite;
|
||||
@@ -1281,6 +1346,7 @@ namespace Stride.Graphics
|
||||
}
|
||||
else
|
||||
{
|
||||
AdoptTransferLayoutIfUndefined(sourceParent, BarrierLayout.CopySource);
|
||||
imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferSrcOptimal;
|
||||
imageBarriers[imageBarrierCount].newLayout = sourceParent.NativeLayout;
|
||||
imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferRead;
|
||||
@@ -1296,6 +1362,7 @@ namespace Stride.Graphics
|
||||
}
|
||||
else
|
||||
{
|
||||
AdoptTransferLayoutIfUndefined(destinationParent, BarrierLayout.CopyDest);
|
||||
imageBarriers[imageBarrierCount].oldLayout = VkImageLayout.TransferDstOptimal;
|
||||
imageBarriers[imageBarrierCount].newLayout = destinationParent.NativeLayout;
|
||||
imageBarriers[imageBarrierCount].srcAccessMask = VkAccessFlags.TransferWrite;
|
||||
@@ -1497,6 +1564,7 @@ namespace Stride.Graphics
|
||||
};
|
||||
GraphicsDevice.NativeDeviceApi.vkCmdCopyBufferToImage(currentCommandList.NativeCommandBuffer, uploadResource, texture.NativeImage, VkImageLayout.TransferDstOptimal, 1, &bufferCopy);
|
||||
|
||||
AdoptTransferLayoutIfUndefined(texture, BarrierLayout.CopyDest);
|
||||
memoryBarrier = new VkImageMemoryBarrier(texture.NativeImage, subresourceRange, VkAccessFlags.TransferWrite, texture.NativeAccessMask, VkImageLayout.TransferDstOptimal, texture.NativeLayout);
|
||||
GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(currentCommandList.NativeCommandBuffer, VkPipelineStageFlags.Transfer, texture.NativePipelineStageMask, VkDependencyFlags.None, memoryBarrierCount: 0, memoryBarriers: null, bufferMemoryBarrierCount: 0, bufferMemoryBarriers: null, imageMemoryBarrierCount: 1, &memoryBarrier);
|
||||
}
|
||||
|
||||
@@ -157,6 +157,14 @@ namespace Stride.Graphics
|
||||
// We use GraphicsDevice (similar to OpenGL)
|
||||
private static readonly Logger Log = GlobalLogger.GetLogger(nameof(GraphicsDevice));
|
||||
|
||||
/// <summary>
|
||||
/// Creates the Vulkan instance and, when requested, its validation layer and debug messenger.
|
||||
/// </summary>
|
||||
/// <param name="enableValidation">Enable the Khronos validation layer.</param>
|
||||
/// <remarks>
|
||||
/// <c>STRIDE_VULKAN_SYNC_VALIDATION=1</c> adds synchronization validation. The validation layer
|
||||
/// provides its extension, so the ICD's instance extension list does not include it.
|
||||
/// </remarks>
|
||||
public unsafe GraphicsAdapterFactoryInstance(bool enableValidation)
|
||||
{
|
||||
var pEngineName = new VkUtf8ReadOnlyString("Stride"u8);
|
||||
@@ -198,7 +206,7 @@ namespace Stride.Graphics
|
||||
Log.Info($"Vulkan validation layer {(enableValidation ? "enabled" : "not found")} for debug instance");
|
||||
}
|
||||
|
||||
var supportedExtensionNames = stackalloc VkUtf8String[]
|
||||
Span<VkUtf8String> supportedExtensionNames = stackalloc VkUtf8String[]
|
||||
{
|
||||
VK_KHR_SURFACE_EXTENSION_NAME,
|
||||
VK_KHR_WIN32_SURFACE_EXTENSION_NAME,
|
||||
@@ -209,8 +217,7 @@ namespace Stride.Graphics
|
||||
VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
|
||||
VK_EXT_DEBUG_UTILS_EXTENSION_NAME
|
||||
};
|
||||
var supportedExtensions = new Span<VkUtf8String>(supportedExtensionNames, 8);
|
||||
var availableExtensionNames = GetAvailableExtensionNames(supportedExtensions);
|
||||
var availableExtensionNames = GetAvailableExtensionNames(supportedExtensionNames);
|
||||
// Surface extensions are optional at instance creation (not available with headless ICDs).
|
||||
// They are validated later when a swapchain is actually created.
|
||||
var desiredExtensionNames = new HashSet<VkUtf8String>();
|
||||
@@ -236,6 +243,11 @@ namespace Stride.Graphics
|
||||
desiredExtensionNames.Add(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
||||
HasDebugUtilsSupport = enableDebugUtils;
|
||||
|
||||
bool enableSyncValidation = enableValidation
|
||||
&& Environment.GetEnvironmentVariable("STRIDE_VULKAN_SYNC_VALIDATION") == "1";
|
||||
if (enableSyncValidation)
|
||||
desiredExtensionNames.Add(VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME);
|
||||
|
||||
using VkStringArray ppEnabledLayerNames = new(enabledLayerNames);
|
||||
using VkStringArray ppEnabledExtensionNames = new(desiredExtensionNames);
|
||||
|
||||
@@ -250,6 +262,16 @@ namespace Stride.Graphics
|
||||
ppEnabledExtensionNames = ppEnabledExtensionNames,
|
||||
};
|
||||
|
||||
var syncValidationFeature = VkValidationFeatureEnableEXT.SynchronizationValidation;
|
||||
var validationFeatures = new VkValidationFeaturesEXT
|
||||
{
|
||||
sType = VkStructureType.ValidationFeaturesEXT,
|
||||
enabledValidationFeatureCount = 1,
|
||||
pEnabledValidationFeatures = &syncValidationFeature,
|
||||
};
|
||||
if (enableSyncValidation)
|
||||
instanceCreateInfo.pNext = &validationFeatures;
|
||||
|
||||
// Silence MoltenVK's per-instance info dump (153-line extension list, device banner).
|
||||
// Set via env var instead of VkLayerSettingsCreateInfoEXT — the layer-settings struct
|
||||
// requires VK_EXT_layer_settings to be enabled, and MoltenVK acting as ICD without the
|
||||
@@ -270,6 +292,7 @@ namespace Stride.Graphics
|
||||
var layerSettings = new VkLayerSettingsCreateInfoEXT
|
||||
{
|
||||
sType = VkStructureType.LayerSettingsCreateInfoEXT,
|
||||
pNext = instanceCreateInfo.pNext,
|
||||
settingCount = 1,
|
||||
pSettings = &mvkLogLevelSetting,
|
||||
};
|
||||
|
||||
@@ -195,6 +195,14 @@ namespace Stride.Graphics
|
||||
AcquireNextImage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the next swapchain image and points the back buffer at it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The tracked layout is reset to <see cref="VkImageLayout.Undefined"/> here rather than seeded to
|
||||
/// Present at creation. A layout transition is a use of the image, and the specification permits
|
||||
/// use only between acquisition and presentation.
|
||||
/// </remarks>
|
||||
private unsafe void AcquireNextImage()
|
||||
{
|
||||
// Get next image
|
||||
@@ -223,6 +231,11 @@ namespace Stride.Graphics
|
||||
// Flip render targets
|
||||
backBuffer.SetNativeHandles(swapchainImages[currentBufferIndex].NativeImage, swapchainImages[currentBufferIndex].NativeColorAttachmentView);
|
||||
|
||||
backBuffer.NativeLayout = VkImageLayout.Undefined;
|
||||
backBuffer.NativeAccessMask = VkAccessFlags.None;
|
||||
backBuffer.NativePipelineStageMask = VkPipelineStageFlags.TopOfPipe;
|
||||
backBuffer.LayoutTracker.Set(uint.MaxValue, BarrierLayout.Undefined);
|
||||
|
||||
lock (GraphicsDevice.QueueLock)
|
||||
{
|
||||
// Signal vkAcquireNextImageKHR Fence => GraphicsDevice.CommandList (so that next command list will wait for this to complete)
|
||||
@@ -626,25 +639,6 @@ namespace Stride.Graphics
|
||||
viewType = VkImageViewType.Image2D,
|
||||
};
|
||||
|
||||
// We initialize swapchain images to PresentSource, since we swap them out while in this layout.
|
||||
backBuffer.NativeAccessMask = VkAccessFlags.MemoryRead;
|
||||
backBuffer.NativeLayout = VkImageLayout.PresentSrcKHR;
|
||||
|
||||
var imageMemoryBarrier = new VkImageMemoryBarrier
|
||||
{
|
||||
sType = VkStructureType.ImageMemoryBarrier,
|
||||
subresourceRange = new VkImageSubresourceRange(VkImageAspectFlags.Color, 0, 1, 0, 1),
|
||||
oldLayout = VkImageLayout.Undefined,
|
||||
newLayout = VkImageLayout.PresentSrcKHR,
|
||||
srcAccessMask = VkAccessFlags.None,
|
||||
dstAccessMask = VkAccessFlags.MemoryRead
|
||||
};
|
||||
|
||||
var commandBuffer = GraphicsDevice.NativeCopyCommandPools.Value.GetObject(0);
|
||||
|
||||
var beginInfo = new VkCommandBufferBeginInfo { sType = VkStructureType.CommandBufferBeginInfo };
|
||||
GraphicsDevice.NativeDeviceApi.vkBeginCommandBuffer(commandBuffer, &beginInfo);
|
||||
|
||||
GraphicsDevice.NativeDeviceApi.vkGetSwapchainImagesKHR(GraphicsDevice.NativeDevice, swapChain, out uint swapchainImageCount);
|
||||
Span<VkImage> buffers = stackalloc VkImage[(int)swapchainImageCount];
|
||||
GraphicsDevice.NativeDeviceApi.vkGetSwapchainImagesKHR(GraphicsDevice.NativeDevice, swapChain, buffers);
|
||||
@@ -655,30 +649,8 @@ namespace Stride.Graphics
|
||||
// Create image views
|
||||
swapchainImages[index].NativeImage = createInfo.image = buffers[index];
|
||||
GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkCreateImageView(GraphicsDevice.NativeDevice, &createInfo, null, out swapchainImages[index].NativeColorAttachmentView));
|
||||
|
||||
// Transition to default layout
|
||||
imageMemoryBarrier.image = buffers[index];
|
||||
GraphicsDevice.NativeDeviceApi.vkCmdPipelineBarrier(commandBuffer, VkPipelineStageFlags.AllCommands, VkPipelineStageFlags.AllCommands, VkDependencyFlags.None, 0, null, 0, null, 1, &imageMemoryBarrier);
|
||||
}
|
||||
|
||||
// Close and submit
|
||||
GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkEndCommandBuffer(commandBuffer));
|
||||
|
||||
lock (GraphicsDevice.QueueLock)
|
||||
{
|
||||
var commandBufferInfo = GraphicsDevice.CommandBufferSubmit(commandBuffer);
|
||||
var submitInfo = new VkSubmitInfo2
|
||||
{
|
||||
sType = VkStructureType.SubmitInfo2,
|
||||
commandBufferInfoCount = 1,
|
||||
pCommandBufferInfos = &commandBufferInfo,
|
||||
};
|
||||
GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkQueueSubmit2(GraphicsDevice.NativeCommandQueue, 1, &submitInfo, VkFence.Null));
|
||||
GraphicsDevice.CheckResult(GraphicsDevice.NativeDeviceApi.vkQueueWaitIdle(GraphicsDevice.NativeCommandQueue));
|
||||
}
|
||||
|
||||
GraphicsDevice.NativeCopyCommandPools.Value.RecycleObject(0, commandBuffer);
|
||||
|
||||
// Create submit semaphores
|
||||
submitSemaphores = new VkSemaphore[buffers.Length];
|
||||
var semaphoreCreateInfo = new VkSemaphoreCreateInfo { sType = VkStructureType.SemaphoreCreateInfo };
|
||||
|
||||
@@ -173,10 +173,10 @@ namespace Stride.Graphics
|
||||
NativeAccessMask = VkAccessFlags.TransferRead;
|
||||
|
||||
if (NativeLayout == VkImageLayout.ColorAttachmentOptimal)
|
||||
NativeAccessMask = VkAccessFlags.ColorAttachmentWrite;
|
||||
NativeAccessMask = VkAccessFlags.ColorAttachmentWrite | VkAccessFlags.ColorAttachmentRead;
|
||||
|
||||
if (NativeLayout == VkImageLayout.DepthStencilAttachmentOptimal)
|
||||
NativeAccessMask = VkAccessFlags.DepthStencilAttachmentWrite;
|
||||
NativeAccessMask = VkAccessFlags.DepthStencilAttachmentWrite | VkAccessFlags.DepthStencilAttachmentRead;
|
||||
|
||||
if (NativeLayout == VkImageLayout.ShaderReadOnlyOptimal)
|
||||
NativeAccessMask = VkAccessFlags.ShaderRead | VkAccessFlags.InputAttachmentRead;
|
||||
@@ -184,7 +184,7 @@ namespace Stride.Graphics
|
||||
NativePipelineStageMask =
|
||||
IsRenderTarget ? VkPipelineStageFlags.ColorAttachmentOutput :
|
||||
IsDepthStencil ? VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests | VkPipelineStageFlags.LateFragmentTests :
|
||||
IsShaderResource || IsUnorderedAccess ? VkPipelineStageFlags.VertexInput | VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader :
|
||||
IsShaderResource || IsUnorderedAccess ? VkPipelineStageFlags.VertexShader | VkPipelineStageFlags.FragmentShader | VkPipelineStageFlags.ComputeShader :
|
||||
VkPipelineStageFlags.None;
|
||||
|
||||
if (ParentTexture != null)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user