diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fab615f..4be3c0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: DOTNET_INSTALL_DIR: ${{ runner.temp }}/.dotnet with: global-json-file: global.json + - name: Verify store listings and media + shell: pwsh + run: ./tools/Test-StoreListings.ps1 - name: Build Unity compile stubs shell: pwsh run: dotnet build tests/OpenGameAgent.Unity.Stubs/OpenGameAgent.Unity.Stubs.csproj -c Release -o artifacts/unity-stubs diff --git a/docs/store-listings/README.md b/docs/store-listings/README.md new file mode 100644 index 0000000..0777f5b --- /dev/null +++ b/docs/store-listings/README.md @@ -0,0 +1,15 @@ +# Store listing source + +This directory is the canonical source for the Godot Asset Store and Unity Asset Store listings. Keep the public forms synchronized with these files. + +- `godot.md`: Godot listing copy and review disclosures. +- `unity.md`: Unity listing copy and review disclosures. +- `artwork/`: editable 16:9 SVG source and rendered upload assets. + +Regenerate PNG upload assets with: + +```powershell +./tools/Render-StoreArtwork.ps1 -Godot godot.exe +``` + +The packages are free, open-source engine adapters. They include the runtime/client surface and a deterministic no-key sample. Real model calls require a separately configured provider or an OpenGameAgent server; no provider account, API key, or paid service is bundled. diff --git a/docs/store-listings/artwork/opengameagent-engine-modes.svg b/docs/store-listings/artwork/opengameagent-engine-modes.svg new file mode 100644 index 0000000..e2562ec --- /dev/null +++ b/docs/store-listings/artwork/opengameagent-engine-modes.svg @@ -0,0 +1,12 @@ + + OpenGameAgent engine modes + OpenGameAgent supports in-process and server-hosted game agent runtimes in Unity and Godot. + + One runtime. Two deployment modes. + Start in the engine. Move authority or credentials to a service when the game needs it. + + IN-PROCESSRuntime inside the gameFast integration · direct host toolsLocal models or player-provided keysUnityGodot .NET + SERVER-HOSTEDRuntime behind an APIProtected credentials · owner authDurable action exchange · scale outSame protocolSidecar + + Provider-neutral · typed streaming · actor-scoped lifecycle · no second agent loop + diff --git a/docs/store-listings/artwork/opengameagent-runtime-flow.svg b/docs/store-listings/artwork/opengameagent-runtime-flow.svg new file mode 100644 index 0000000..1897df4 --- /dev/null +++ b/docs/store-listings/artwork/opengameagent-runtime-flow.svg @@ -0,0 +1,17 @@ + + OpenGameAgent runtime flow + Structured world state flows through the agent loop into typed intents that the authoritative game validates. + + From world state to safe game action + The model proposes. Your game remains authoritative. + + 01 · OBSERVEStructured contextJSON state · game timeimages · events · memory + + 02 · THINKAgent looproute · plan · streamsteer · tools · retry + + 03 · PROPOSETyped intentbounded schema · operation IDexpected world revision + + GAME COMMITS + + Validation · permissions · resources · physics · revisions remain ordinary game code + diff --git a/docs/store-listings/artwork/opengameagent-store-hero.svg b/docs/store-listings/artwork/opengameagent-store-hero.svg new file mode 100644 index 0000000..1a40f13 --- /dev/null +++ b/docs/store-listings/artwork/opengameagent-store-hero.svg @@ -0,0 +1,31 @@ + + OpenGameAgent + Provider-neutral agent runtime for AI-native games. + + + + + + + + + + + + + + + + + + OpenGameAgent + Agent runtime for AI-native games + + Structured context + Typed tools + Streaming + Multi-NPC runtime + + Unity 6 • Godot 4.7 .NET • In-process or server-hosted + MIT licensed · opengameagent.com + diff --git a/docs/store-listings/artwork/rendered/opengameagent-engine-modes.png b/docs/store-listings/artwork/rendered/opengameagent-engine-modes.png new file mode 100644 index 0000000..d701e16 Binary files /dev/null and b/docs/store-listings/artwork/rendered/opengameagent-engine-modes.png differ diff --git a/docs/store-listings/artwork/rendered/opengameagent-runtime-flow.png b/docs/store-listings/artwork/rendered/opengameagent-runtime-flow.png new file mode 100644 index 0000000..2cb4feb Binary files /dev/null and b/docs/store-listings/artwork/rendered/opengameagent-runtime-flow.png differ diff --git a/docs/store-listings/artwork/rendered/opengameagent-store-hero.png b/docs/store-listings/artwork/rendered/opengameagent-store-hero.png new file mode 100644 index 0000000..5697052 Binary files /dev/null and b/docs/store-listings/artwork/rendered/opengameagent-store-hero.png differ diff --git a/docs/store-listings/godot.md b/docs/store-listings/godot.md new file mode 100644 index 0000000..5bcdada --- /dev/null +++ b/docs/store-listings/godot.md @@ -0,0 +1,57 @@ +# Godot Asset Store listing + +## Identity + +- Publisher: **OpenGameAgent** +- Asset name: **OpenGameAgent** +- Slug: `opengameagent` +- Price: **Free** +- License: **MIT** +- Category: **Tools / AI** +- Minimum engine: **Godot 4.7 .NET** +- Source: +- Website: + +## Summary + +Provider-neutral agent runtime for AI-native Godot games. Give NPCs structured world context, tool use, streaming responses, steering, and independent concurrent lives. + +## Description + +OpenGameAgent is an open-source runtime for building AI-native game characters and systems. It turns structured game state into an agent loop that can reason, call typed game tools, stream results, and continue across game time—without moving game authority into the model. + +The Godot 4.7 .NET add-on provides an `OpenGameAgentNode` that runs either in process or against an OpenGameAgent server. It exposes typed C# methods plus bounded, main-thread Godot signals for GDScript and UI integration. + +### Included + +- Structured `GameInput` with arbitrary JSON context and game-time moments +- Stateful agent/tool loop with streaming lifecycle events +- Local in-process and remote server modes +- Actor-scoped steering, abort, cancellation, and bounded callback delivery +- Multi-NPC concurrency through isolated actor lanes +- Shared runtime/client assemblies +- Deterministic no-key example +- MIT license and source code + +### Game authority + +Models propose typed intents. Your game still validates rules, resources, revisions, permissions, physics, pathfinding, and final state changes. Optional OpenGameAgent packages add durable actions, memory, scheduling, workflows, media providers, MCP, and other capabilities. + +### Providers and privacy + +No model or paid service is bundled. OpenGameAgent itself does not collect or transmit project data. Data leaves the game only through providers or services explicitly configured by the developer. Do not embed a permanent provider key in an exported client; use BYOK, a local endpoint, short-lived credentials, or a server. + +### Installation + +Copy `addons/open_game_agent` into a Godot 4.7 .NET project, import `OpenGameAgent.Godot.props` from the project `.csproj`, and add `OpenGameAgentNode` to a scene or Autoload. Full instructions are included and maintained online. + +## Search tags + +`AI`, `agent`, `NPC`, `LLM`, `tools`, `memory`, `multi-agent`, `simulation`, `runtime`, `Godot C#` + +## Review notes + +- The add-on is a Godot 4.7 .NET runtime library; the GDScript editor plug-in is intentionally side-effect free. +- The bundled sample is deterministic and makes no network request. +- External provider packages, accounts, API terms, costs, and data policies are not included. +- Package path is `addons/open_game_agent` and the license is present inside that directory. diff --git a/docs/store-listings/unity.md b/docs/store-listings/unity.md new file mode 100644 index 0000000..598ec5e --- /dev/null +++ b/docs/store-listings/unity.md @@ -0,0 +1,58 @@ +# Unity Asset Store listing + +## Identity + +- Publisher: **OpenGameAgent** +- Product: **OpenGameAgent — Agent Runtime for AI-Native Games** +- Technical package: `com.opengameagent.runtime` +- Price: **Free** +- License: **MIT** (open-source SDK/tool; review under the applicable Unity package terms) +- Category: **Tools / AI** +- Minimum editor: **Unity 6 (6000.0.0f1)** +- Tested editor: **Unity 6000.5.6f1 on Windows** +- Source: +- Website: + +## Short description + +Provider-neutral agent runtime for AI-native Unity games: structured context, typed tools, streaming, steering, local/server execution, and multi-NPC concurrency. + +## Description + +OpenGameAgent is an open-source runtime for building AI-native game characters and systems. Each NPC can receive structured world state, reason through an agent loop, call typed game tools, stream its response, and keep an independent life across game time. + +The Unity package provides `OpenGameAgentBehaviour` plus shared runtime/client assemblies. Run agents in the Unity process for a simple local architecture, or connect to an OpenGameAgent service when credentials, server authority, or scale belong outside the player. + +### Included + +- Structured JSON game context and explicit game-time moments +- Stateful agent/tool loop with streamed lifecycle events +- Local in-process and remote server modes +- Actor-scoped steering, abort, cancellation, and bounded main-thread callbacks +- Multi-NPC concurrency through isolated actor lanes +- Deterministic no-key sample importable from Package Manager +- Complete MIT and third-party notices + +### Designed for authoritative games + +Models propose typed intents. Your game validates rules, resources, revisions, permissions, physics, pathfinding, and final state changes. Optional OpenGameAgent packages add durable actions, memory, scheduling, workflows, media providers, MCP, and other capabilities. + +### Providers, keys, and privacy + +No model, provider account, API key, or paid service is bundled. OpenGameAgent itself does not collect or transmit project data. Data leaves the game only through providers or services explicitly configured by the developer. A permanent key embedded in a player can be extracted; use BYOK, a local endpoint, developer-issued short-lived credentials, or a server. + +### Getting started + +Install the UPM package, import **Minimal Local Agent** from the Samples tab, add `OpenGameAgentQuickstart` to a GameObject, and enter Play Mode. Then replace the deterministic sample provider with the provider or server configuration your game uses. + +## Search tags + +`AI`, `agent`, `NPC`, `LLM`, `tools`, `memory`, `multi-agent`, `simulation`, `runtime`, `Unity` + +## Dependencies and review disclosures + +- Bundles OpenGameAgent runtime/client assemblies and .NET support assemblies listed in `Third-Party Notices.txt`. +- No external service is required for the included sample. +- Real model use requires a separately configured provider or OpenGameAgent server and may have separate terms or charges. +- The package never asks for or stores a provider key. Credential handling is chosen by the developer; permanent keys must not be shipped in player builds. +- Package code does not use project/customer data to train any model. diff --git a/engines/godot/addons/open_game_agent/LICENSE b/engines/godot/addons/open_game_agent/LICENSE new file mode 100644 index 0000000..3a4b026 --- /dev/null +++ b/engines/godot/addons/open_game_agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eric Sun and OpenGameAgent contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/engines/godot/addons/open_game_agent/README.md b/engines/godot/addons/open_game_agent/README.md index 91341af..caab604 100644 --- a/engines/godot/addons/open_game_agent/README.md +++ b/engines/godot/addons/open_game_agent/README.md @@ -7,10 +7,16 @@ Godot 4.7 .NET adapter for the OpenGameAgent runtime. 3. Add `OpenGameAgentNode` to a scene or Autoload. 4. Call `Configure(runtime)` for in-process execution or `ConfigureRemote(client)` for a .NET service. +For a no-key first run, copy `examples/minimal_local_agent/MinimalLocalAgent.cs` into your own scene script or instantiate its node from C#. It uses a deterministic provider and performs no network request. + Use the typed async methods from C#. `SteerActorAsync` and `AbortActorAsync` work in local and remote modes. For GDScript, call `RunJson` and connect to `run_event`, `run_completed`, and `run_failed`. Stream signals use a bounded queue drained on the Godot thread; optional `Configure` arguments tune active runs, queue capacity, and per-frame budget. Admitted runs reserve terminal delivery. The distributable add-on contains its shared runtime DLLs. Generated packages belong in `engines/godot/artifacts` and are not committed. Persistence, provider, official extension, model-catalog, and external-tool packages are versioned separately so a project can include only what it needs. A permanent model key embedded in an exported game can be extracted; use BYOK, a local endpoint, or developer-issued short-lived credentials. +OpenGameAgent itself does not collect or transmit project data. Data leaves the game only through providers or services explicitly configured by the developer. Provider accounts, API terms, usage charges, and data handling are controlled by those third parties. + +This add-on is open source under the MIT License. See `LICENSE` in this directory. + Full setup, lifecycle, cancellation, and main-thread guidance is in `docs/engine-integration.md` at the repository root. diff --git a/engines/godot/addons/open_game_agent/examples/minimal_local_agent/MinimalLocalAgent.cs b/engines/godot/addons/open_game_agent/examples/minimal_local_agent/MinimalLocalAgent.cs new file mode 100644 index 0000000..0e477f1 --- /dev/null +++ b/engines/godot/addons/open_game_agent/examples/minimal_local_agent/MinimalLocalAgent.cs @@ -0,0 +1,46 @@ +#nullable enable + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Godot; +using OpenGameAgent.Kernel; + +namespace OpenGameAgent.Godot.Samples; + +public partial class MinimalLocalAgent : Node +{ + public override async void _Ready() + { + await using var runtime = new GameAgentRuntime(new GameAgentRuntimeOptions( + new DeterministicProvider(), + "sample-model")); + var input = new GameInput( + "sample-session", + "guide", + "player.interaction", + "{\"message\":\"Hello from Godot\",\"nearbyObjects\":[\"campfire\"]}", + new GameMoment("sample-world", 1), + "godot-sample-1"); + + var result = await runtime.RunAsync(input); + GD.Print($"OpenGameAgent sample completed: {result.Status}"); + } + + private sealed class DeterministicProvider : IModelProvider + { + public async IAsyncEnumerable StreamAsync( + ModelRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + _ = request; + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return ModelStreamEvent.Terminal(new ModelResponse( + new AgentContent[] { new TextContent("Welcome to the world.") }, + ModelStopReason.Stop, + new ModelUsage(5, 5))); + } + } +} diff --git a/engines/godot/addons/open_game_agent/examples/minimal_local_agent/README.md b/engines/godot/addons/open_game_agent/examples/minimal_local_agent/README.md new file mode 100644 index 0000000..bca9a98 --- /dev/null +++ b/engines/godot/addons/open_game_agent/examples/minimal_local_agent/README.md @@ -0,0 +1,5 @@ +# Minimal local agent + +Attach `MinimalLocalAgent.cs` to a `Node` in a Godot 4.7 .NET project that imports `OpenGameAgent.Godot.props`, then run the scene. The output panel prints `OpenGameAgent sample completed`. + +The sample uses a deterministic provider, requires no API key, and performs no network request. diff --git a/engines/godot/addons/open_game_agent/plugin.cfg b/engines/godot/addons/open_game_agent/plugin.cfg index d480d95..5c8020c 100644 --- a/engines/godot/addons/open_game_agent/plugin.cfg +++ b/engines/godot/addons/open_game_agent/plugin.cfg @@ -1,7 +1,7 @@ [plugin] name="OpenGameAgent" -description="Game-native agent runtime node for Godot." +description="Provider-neutral agent runtime for AI-native Godot games." author="Eric Sun" version="0.3.0-alpha.2" -script="" +script="plugin.gd" diff --git a/engines/godot/addons/open_game_agent/plugin.gd b/engines/godot/addons/open_game_agent/plugin.gd new file mode 100644 index 0000000..710c38a --- /dev/null +++ b/engines/godot/addons/open_game_agent/plugin.gd @@ -0,0 +1,6 @@ +@tool +extends EditorPlugin + +# The runtime is exposed by OpenGameAgentNode in the .NET assembly. Keeping this +# EditorPlugin intentionally side-effect free makes the add-on discoverable and +# allows projects to enable or disable it without changing scenes or settings. diff --git a/engines/godot/build-package.ps1 b/engines/godot/build-package.ps1 index f13a3ce..762d55d 100644 --- a/engines/godot/build-package.ps1 +++ b/engines/godot/build-package.ps1 @@ -43,9 +43,12 @@ if (Test-Path -LiteralPath $packageRoot) { New-Item -ItemType Directory -Path (Join-Path $packagedAddon 'runtime'), (Join-Path $packagedAddon 'lib') -Force | Out-Null Copy-Item -LiteralPath (Join-Path $repositoryRoot 'LICENSE') -Destination (Join-Path $packageRoot 'LICENSE') +Copy-Item -LiteralPath (Join-Path $addonRoot 'LICENSE') -Destination (Join-Path $packagedAddon 'LICENSE') Copy-Item -LiteralPath (Join-Path $addonRoot 'runtime\OpenGameAgentNode.cs') -Destination (Join-Path $packagedAddon 'runtime\OpenGameAgentNode.cs') Copy-Item -LiteralPath (Join-Path $addonRoot 'OpenGameAgent.Godot.props') -Destination (Join-Path $packagedAddon 'OpenGameAgent.Godot.props') Copy-Item -LiteralPath (Join-Path $addonRoot 'README.md') -Destination (Join-Path $packagedAddon 'README.md') +Copy-Item -LiteralPath (Join-Path $addonRoot 'plugin.gd') -Destination (Join-Path $packagedAddon 'plugin.gd') +Copy-Item -LiteralPath (Join-Path $addonRoot 'examples') -Destination (Join-Path $packagedAddon 'examples') -Recurse $plugin = Get-Content -LiteralPath (Join-Path $addonRoot 'plugin.cfg') -Raw $plugin = $plugin -replace 'version="[^"]+"', ('version="' + $Version + '"') $plugin | Set-Content -LiteralPath (Join-Path $packagedAddon 'plugin.cfg') -Encoding utf8NoBOM diff --git a/engines/godot/test-package.ps1 b/engines/godot/test-package.ps1 index 33aacce..06c3463 100644 --- a/engines/godot/test-package.ps1 +++ b/engines/godot/test-package.ps1 @@ -15,9 +15,12 @@ $packageRoot = [string]$buildOutput[-1] $required = @( 'LICENSE', + 'addons\open_game_agent\LICENSE', 'addons\open_game_agent\plugin.cfg', + 'addons\open_game_agent\plugin.gd', 'addons\open_game_agent\OpenGameAgent.Godot.props', 'addons\open_game_agent\runtime\OpenGameAgentNode.cs', + 'addons\open_game_agent\examples\minimal_local_agent\MinimalLocalAgent.cs', 'addons\open_game_agent\lib\OpenGameAgent.Attachments.dll', 'addons\open_game_agent\lib\OpenGameAgent.Kernel.dll', 'addons\open_game_agent\lib\OpenGameAgent.dll', @@ -34,5 +37,9 @@ $packagedLicenseHash = (Get-FileHash -LiteralPath (Join-Path $packageRoot 'LICEN if ($packagedLicenseHash -ne $sourceLicenseHash) { throw 'Godot package must contain the complete repository license.' } +$addonLicenseHash = (Get-FileHash -LiteralPath (Join-Path $packageRoot 'addons\open_game_agent\LICENSE') -Algorithm SHA256).Hash +if ($addonLicenseHash -ne $sourceLicenseHash) { + throw 'Godot add-on directory must contain the complete repository license.' +} $packageRoot diff --git a/engines/unity/Packages/com.opengameagent.runtime/README.md b/engines/unity/Packages/com.opengameagent.runtime/README.md index 8b7d007..9f9a70a 100644 --- a/engines/unity/Packages/com.opengameagent.runtime/README.md +++ b/engines/unity/Packages/com.opengameagent.runtime/README.md @@ -7,10 +7,16 @@ Unity 6 adapter for the OpenGameAgent runtime. 3. Add `OpenGameAgentBehaviour` to a persistent GameObject. 4. Call `Configure(runtime)` for in-process execution or `ConfigureRemote(client)` for a .NET service. +For a no-key first run, import **Minimal Local Agent** from Package Manager > OpenGameAgent > Samples, add `OpenGameAgentQuickstart` to a GameObject, and enter Play Mode. The sample uses a deterministic provider so it never contacts an external service. + The component exposes typed async methods, a JSON start method, actor-scoped steering and abort in local or remote mode, bounded main-thread event delivery with reserved terminal callbacks, cancellation, and teardown safety. Subscribe to `RunEvent`, `RunCompleted`, and `RunFailed` in code or the Inspector. Unity calls `PumpCallbacks()` from `Update`; custom player loops may call it explicitly. Generated package DLLs belong in the repository-level `artifacts/unity` directory and are not committed. The base UPM package contains the adapter plus shared runtime/client assemblies. Persistence, provider, official extension, model-catalog, and external-tool packages are versioned separately so each game can choose its deployment surface. A permanent model key embedded in a Unity player can be extracted; use BYOK, a local endpoint, or developer-issued short-lived credentials. +OpenGameAgent itself does not collect or transmit project data. Data leaves the game only through providers or services explicitly configured by the developer. Provider accounts, API terms, usage charges, and data handling are controlled by those third parties. Never put a permanent provider key in a shipped player build. + +This package is open source under the MIT License. See `LICENSE.md` and `Third-Party Notices.txt` in the package root. + Full setup, credentials, lifecycle, and main-thread guidance is in the repository's [engine integration guide](https://github.com/EricSun0218/OpenGameAgent/blob/main/docs/engine-integration.md). diff --git a/engines/unity/Packages/com.opengameagent.runtime/Samples~.meta b/engines/unity/Packages/com.opengameagent.runtime/Samples~.meta new file mode 100644 index 0000000..ef31d29 --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Samples~.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6fd5a7b267ef43849f4a5639ae108123 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent.meta b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent.meta new file mode 100644 index 0000000..c7f9c36 --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59edcab3267d41e0a91456c2f1037274 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/OpenGameAgentQuickstart.cs b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/OpenGameAgentQuickstart.cs new file mode 100644 index 0000000..198af83 --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/OpenGameAgentQuickstart.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using OpenGameAgent.Kernel; +using UnityEngine; + +namespace OpenGameAgent.Unity.Samples +{ + +public sealed class OpenGameAgentQuickstart : MonoBehaviour +{ + private async void Start() + { + var runtime = new GameAgentRuntime(new GameAgentRuntimeOptions( + new DeterministicProvider(), + "sample-model")); + var input = new GameInput( + "sample-session", + "guide", + "player.interaction", + "{\"message\":\"Hello from Unity\",\"nearbyObjects\":[\"campfire\"]}", + new GameMoment("sample-world", 1), + "unity-sample-1"); + + var result = await runtime.RunAsync(input, destroyCancellationToken); + Debug.Log($"OpenGameAgent sample completed: {result.Status}"); + await runtime.DisposeAsync(); + } + + private sealed class DeterministicProvider : IModelProvider + { + public async IAsyncEnumerable StreamAsync( + ModelRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + _ = request; + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return ModelStreamEvent.Terminal(new ModelResponse( + new AgentContent[] { new TextContent("Welcome to the world.") }, + ModelStopReason.Stop, + new ModelUsage(5, 5))); + } + } +} + +} diff --git a/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/OpenGameAgentQuickstart.cs.meta b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/OpenGameAgentQuickstart.cs.meta new file mode 100644 index 0000000..701317d --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/OpenGameAgentQuickstart.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f562ef3536534a7f9910a23b54d1d24b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/README.md b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/README.md new file mode 100644 index 0000000..a250baf --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/README.md @@ -0,0 +1,7 @@ +# Minimal Local Agent + +1. Add `OpenGameAgentQuickstart` to an empty GameObject. +2. Enter Play Mode. +3. Check the Console for `OpenGameAgent sample completed`. + +The sample deliberately uses a deterministic provider. It requires no API key, performs no network request, and demonstrates the same `GameInput -> GameAgentRuntime -> streamed model response` path used by a real provider. diff --git a/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/README.md.meta b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/README.md.meta new file mode 100644 index 0000000..45ac96c --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Samples~/Minimal Local Agent/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 334227155bc54a56a8ff64a95fd87874 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/engines/unity/Packages/com.opengameagent.runtime/Third-Party Notices.txt b/engines/unity/Packages/com.opengameagent.runtime/Third-Party Notices.txt new file mode 100644 index 0000000..c70277c --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Third-Party Notices.txt @@ -0,0 +1,36 @@ +OpenGameAgent Unity package third-party notices +================================================ + +The generated Unity package includes the following .NET runtime support assemblies: + +- Microsoft.Bcl.AsyncInterfaces +- System.Buffers +- System.Memory +- System.Numerics.Vectors +- System.Runtime.CompilerServices.Unsafe +- System.Text.Encodings.Web +- System.Text.Json +- System.Threading.Tasks.Extensions + +These components are provided by the .NET Foundation and contributors under the MIT License: +https://github.com/dotnet/runtime/blob/main/LICENSE.TXT + +Copyright (c) .NET Foundation and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/engines/unity/Packages/com.opengameagent.runtime/Third-Party Notices.txt.meta b/engines/unity/Packages/com.opengameagent.runtime/Third-Party Notices.txt.meta new file mode 100644 index 0000000..9785fbe --- /dev/null +++ b/engines/unity/Packages/com.opengameagent.runtime/Third-Party Notices.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 661f83096f1342c59f3e5d669f7bc442 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/engines/unity/Packages/com.opengameagent.runtime/package.json b/engines/unity/Packages/com.opengameagent.runtime/package.json index 1765863..123ea7e 100644 --- a/engines/unity/Packages/com.opengameagent.runtime/package.json +++ b/engines/unity/Packages/com.opengameagent.runtime/package.json @@ -2,8 +2,12 @@ "name": "com.opengameagent.runtime", "version": "0.3.0-alpha.2", "displayName": "OpenGameAgent", - "description": "In-process and server-hosted agent runtime for AI-native Unity games.", + "description": "A provider-neutral agent runtime for AI-native Unity games, with local and server-hosted execution, structured context, tools, streaming, steering, and multi-actor coordination.", "unity": "6000.0", + "unityRelease": "0f1", + "documentationUrl": "https://github.com/EricSun0218/OpenGameAgent/blob/main/docs/engine-integration.md", + "changelogUrl": "https://github.com/EricSun0218/OpenGameAgent/blob/main/engines/unity/Packages/com.opengameagent.runtime/CHANGELOG.md", + "licensesUrl": "https://github.com/EricSun0218/OpenGameAgent/blob/main/LICENSE", "keywords": [ "ai", "agent", @@ -13,6 +17,14 @@ "multi-agent" ], "author": { - "name": "Eric Sun" - } + "name": "Eric Sun", + "url": "https://opengameagent.com" + }, + "samples": [ + { + "displayName": "Minimal Local Agent", + "description": "Runs a deterministic, no-key agent turn and prints the result.", + "path": "Samples~/Minimal Local Agent" + } + ] } diff --git a/engines/unity/test-editor.ps1 b/engines/unity/test-editor.ps1 index 4ef4f80..f8f2d44 100644 --- a/engines/unity/test-editor.ps1 +++ b/engines/unity/test-editor.ps1 @@ -32,14 +32,18 @@ if ($creation.ExitCode -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $testRo } $assetsEditor = Join-Path $testRoot 'Assets\Editor' +$assetsSample = Join-Path $testRoot 'Assets\OpenGameAgentSample' $packages = Join-Path $testRoot 'Packages' -New-Item -ItemType Directory -Path $assetsEditor -Force | Out-Null +New-Item -ItemType Directory -Path $assetsEditor, $assetsSample -Force | Out-Null try { $packageUri = 'file:' + ([IO.Path]::GetFullPath($packagePath).Replace('\', '/')) $manifestPath = Join-Path $packages 'manifest.json' $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json $manifest.dependencies | Add-Member -NotePropertyName 'com.opengameagent.runtime' -NotePropertyValue $packageUri -Force $manifest | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM + Copy-Item ` + -LiteralPath (Join-Path $packagePath 'Samples~\Minimal Local Agent\OpenGameAgentQuickstart.cs') ` + -Destination (Join-Path $assetsSample 'OpenGameAgentQuickstart.cs') @' using System; using System.Collections.Generic; diff --git a/engines/unity/test-package.ps1 b/engines/unity/test-package.ps1 index a840dab..898e6e3 100644 --- a/engines/unity/test-package.ps1 +++ b/engines/unity/test-package.ps1 @@ -14,6 +14,7 @@ $packagePath = [string]$buildOutput[-1] $required = @( 'LICENSE.md', + 'Third-Party Notices.txt', 'package.json', 'package.json.meta', 'Runtime.meta', @@ -31,6 +32,7 @@ $required = @( 'Runtime\Plugins\OpenGameAgent.Client.dll', 'Runtime\Plugins\OpenGameAgent.Client.dll.meta', 'Runtime\Plugins\System.Text.Json.dll' + 'Samples~\Minimal Local Agent\OpenGameAgentQuickstart.cs' ) foreach ($relative in $required) { if (-not (Test-Path -LiteralPath (Join-Path $packagePath $relative) -PathType Leaf)) { @@ -52,7 +54,11 @@ if ($unexpected) { } $manifest = Get-Content -LiteralPath (Join-Path $packagePath 'package.json') -Raw | ConvertFrom-Json -if ($manifest.name -ne 'com.opengameagent.runtime' -or $manifest.version -ne $Version) { +if ($manifest.name -ne 'com.opengameagent.runtime' -or + $manifest.version -ne $Version -or + $manifest.unity -ne '6000.0' -or + $manifest.unityRelease -ne '0f1' -or + $manifest.samples.Count -ne 1) { throw 'Unity package manifest identity is invalid.' } diff --git a/tools/Render-StoreArtwork.ps1 b/tools/Render-StoreArtwork.ps1 new file mode 100644 index 0000000..d361b3a --- /dev/null +++ b/tools/Render-StoreArtwork.ps1 @@ -0,0 +1,165 @@ +[CmdletBinding()] +param( + [string] $Godot = 'godot.exe' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$sourceRoot = Join-Path $repositoryRoot 'docs\store-listings\artwork' +$outputRoot = Join-Path $sourceRoot 'rendered' +$script = Join-Path $PSScriptRoot 'render-store-artwork.gd' +New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null + +Add-Type -AssemblyName System.Drawing + +function Add-Text { + param( + [Parameter(Mandatory = $true)] [System.Drawing.Graphics] $Graphics, + [Parameter(Mandatory = $true)] [string] $Text, + [Parameter(Mandatory = $true)] [float] $X, + [Parameter(Mandatory = $true)] [float] $Y, + [Parameter(Mandatory = $true)] [float] $Size, + [Parameter(Mandatory = $true)] [string] $Color, + [switch] $Bold + ) + + $style = if ($Bold) { [Drawing.FontStyle]::Bold } else { [Drawing.FontStyle]::Regular } + $font = [Drawing.Font]::new('Segoe UI', $Size, $style, [Drawing.GraphicsUnit]::Pixel) + $brush = [Drawing.SolidBrush]::new([Drawing.ColorTranslator]::FromHtml($Color)) + try { + $Graphics.DrawString($Text, $font, $brush, $X, $Y) + } + finally { + $brush.Dispose() + $font.Dispose() + } +} + +function Add-TextOverlay { + param( + [Parameter(Mandatory = $true)] [string] $ImagePath, + [Parameter(Mandatory = $true)] [string] $ArtworkName + ) + + $sourceImage = [Drawing.Image]::FromFile($ImagePath) + $bitmap = [Drawing.Bitmap]::new($sourceImage) + $sourceImage.Dispose() + $graphics = [Drawing.Graphics]::FromImage($bitmap) + $graphics.TextRenderingHint = [Drawing.Text.TextRenderingHint]::AntiAliasGridFit + try { + switch ($ArtworkName) { + 'opengameagent-store-hero' { + $lightPen = [Drawing.Pen]::new([Drawing.ColorTranslator]::FromHtml('#EEE9DC'), 33.6) + $accentPen = [Drawing.Pen]::new([Drawing.ColorTranslator]::FromHtml('#DE593C'), 33.6) + $letterPen = [Drawing.Pen]::new([Drawing.ColorTranslator]::FromHtml('#EEE9DC'), 24) + foreach ($pen in @($lightPen, $accentPen, $letterPen)) { + $pen.StartCap = [Drawing.Drawing2D.LineCap]::Square + $pen.EndCap = [Drawing.Drawing2D.LineCap]::Square + $pen.LineJoin = [Drawing.Drawing2D.LineJoin]::Miter + } + try { + [Drawing.PointF[]] $points = @( + [Drawing.PointF]::new(338.4,210.4), [Drawing.PointF]::new(216,210.4), + [Drawing.PointF]::new(158.4,268), [Drawing.PointF]::new(158.4,383.2), + [Drawing.PointF]::new(216,440.8), [Drawing.PointF]::new(331.2,440.8), + [Drawing.PointF]::new(388.8,383.2), [Drawing.PointF]::new(388.8,325.6), + [Drawing.PointF]::new(309.6,325.6)) + $graphics.DrawLines($lightPen, $points) + $graphics.DrawLine($accentPen, 388.8, 325.6, 336, 325.6) + [Drawing.PointF[]] $letterPoints = @( + [Drawing.PointF]::new(232.8,383.2), + [Drawing.PointF]::new(273.6,268), + [Drawing.PointF]::new(314.4,383.2)) + $graphics.DrawLines($letterPen, $letterPoints) + $graphics.DrawLine($letterPen, 249.6, 340, 297.6, 340) + } + finally { + $lightPen.Dispose() + $accentPen.Dispose() + $letterPen.Dispose() + } + Add-Text $graphics 'OpenGameAgent' 500 280 104 '#EEE9DC' -Bold + Add-Text $graphics 'Agent runtime for AI-native games' 505 405 38 '#B8C1D1' + Add-Text $graphics 'Structured context' 530 536 23 '#EEE9DC' -Bold + Add-Text $graphics 'Typed tools' 786 536 23 '#EEE9DC' -Bold + Add-Text $graphics 'Streaming' 989 536 23 '#EEE9DC' -Bold + Add-Text $graphics 'Multi-NPC runtime' 1180 536 23 '#FFFFFF' -Bold + Add-Text $graphics 'Unity 6 • Godot 4.7 .NET • In-process or server-hosted' 505 625 26 '#718097' + Add-Text $graphics 'MIT licensed · opengameagent.com' 128 765 23 '#718097' + } + 'opengameagent-runtime-flow' { + Add-Text $graphics 'From world state to safe game action' 100 65 64 '#EEE9DC' -Bold + Add-Text $graphics 'The model proposes. Your game remains authoritative.' 100 150 28 '#8997AC' + foreach ($item in @( + @('01 · OBSERVE',126,342,22,'#DE593C',$true), @('Structured context',126,388,38,'#EEE9DC',$true), + @('JSON state · game time',126,459,24,'#AEB9C9',$false), @('images · events · memory',126,496,24,'#AEB9C9',$false), + @('02 · THINK',576,342,22,'#DE593C',$true), @('Agent loop',576,388,38,'#EEE9DC',$true), + @('route · plan · stream',576,459,24,'#AEB9C9',$false), @('steer · tools · retry',576,496,24,'#AEB9C9',$false), + @('03 · PROPOSE',1026,342,22,'#DE593C',$true), @('Typed intent',1026,388,38,'#EEE9DC',$true), + @('bounded schema · operation ID',1026,459,24,'#AEB9C9',$false), @('expected world revision',1026,496,24,'#AEB9C9',$false), + @('GAME COMMITS',1309,676,26,'#FFFFFF',$true), + @('Validation · permissions · resources · physics · revisions remain ordinary game code',100,758,24,'#718097',$false))) { + Add-Text $graphics $item[0] $item[1] $item[2] $item[3] $item[4] -Bold:$item[5] + } + } + 'opengameagent-engine-modes' { + Add-Text $graphics 'One runtime. Two deployment modes.' 100 65 64 '#EEE9DC' -Bold + Add-Text $graphics 'Start in the engine. Move authority or credentials to a service when the game needs it.' 100 150 28 '#8997AC' + foreach ($item in @( + @('IN-PROCESS',145,342,22,'#DE593C',$true), @('Runtime inside the game',145,388,45,'#EEE9DC',$true), + @('Fast integration · direct host tools',145,482,27,'#B8C1D1',$false), @('Local models or player-provided keys',145,529,27,'#B8C1D1',$false), + @('Unity',234,616,27,'#EEE9DC',$false), @('Godot .NET',390,616,27,'#EEE9DC',$false), + @('SERVER-HOSTED',895,342,22,'#DE593C',$true), @('Runtime behind an API',895,388,45,'#EEE9DC',$true), + @('Protected credentials · owner auth',895,482,27,'#B8C1D1',$false), @('Durable action exchange · scale out',895,529,27,'#B8C1D1',$false), + @('Same protocol',968,617,25,'#FFFFFF',$false), @('Sidecar',1217,616,27,'#EEE9DC',$false), + @('Provider-neutral · typed streaming · actor-scoped lifecycle · no second agent loop',100,768,24,'#718097',$false))) { + Add-Text $graphics $item[0] $item[1] $item[2] $item[3] $item[4] -Bold:$item[5] + } + } + default { throw "No text overlay is defined for '$ArtworkName'." } + } + + $bitmap.Save($ImagePath, [Drawing.Imaging.ImageFormat]::Png) + } + finally { + $graphics.Dispose() + $bitmap.Dispose() + } +} + +foreach ($source in Get-ChildItem -LiteralPath $sourceRoot -File -Filter '*.svg' | Sort-Object Name) { + $destination = Join-Path $outputRoot ($source.BaseName + '.png') + $render = Start-Process ` + -FilePath $Godot ` + -ArgumentList @( + '--headless', + '--script', + ('"{0}"' -f $script), + '--', + ('"{0}"' -f $source.FullName), + ('"{0}"' -f $destination)) ` + -Wait ` + -PassThru ` + -WindowStyle Hidden + if ($render.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $destination -PathType Leaf)) { + throw "Failed to render '$($source.Name)'." + } + + Add-TextOverlay -ImagePath $destination -ArtworkName $source.BaseName + + $bytes = [IO.File]::ReadAllBytes($destination) + if ($bytes.Length -lt 24 -or + $bytes[0] -ne 0x89 -or $bytes[1] -ne 0x50 -or $bytes[2] -ne 0x4E -or $bytes[3] -ne 0x47) { + throw "Rendered artwork '$($source.Name)' is not a PNG." + } + + $width = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 16)) + $height = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 20)) + if ($width -ne 1600 -or $height -ne 900) { + throw "Rendered artwork '$($source.Name)' must be 1600x900, got ${width}x${height}." + } +} + +$outputRoot diff --git a/tools/Test-StoreListings.ps1 b/tools/Test-StoreListings.ps1 new file mode 100644 index 0000000..40470e3 --- /dev/null +++ b/tools/Test-StoreListings.ps1 @@ -0,0 +1,93 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path -Parent $PSScriptRoot + +foreach ($relative in @( + 'docs\store-listings\godot.md', + 'docs\store-listings\unity.md', + 'engines\godot\addons\open_game_agent\LICENSE', + 'engines\godot\addons\open_game_agent\plugin.gd', + 'engines\godot\addons\open_game_agent\examples\minimal_local_agent\MinimalLocalAgent.cs', + 'engines\unity\Packages\com.opengameagent.runtime\Third-Party Notices.txt', + 'engines\unity\Packages\com.opengameagent.runtime\Samples~\Minimal Local Agent\OpenGameAgentQuickstart.cs')) { + if (-not (Test-Path -LiteralPath (Join-Path $repositoryRoot $relative) -PathType Leaf)) { + throw "Store distribution is missing '$relative'." + } +} + +$manifest = Get-Content -LiteralPath ( + Join-Path $repositoryRoot 'engines\unity\Packages\com.opengameagent.runtime\package.json') -Raw | + ConvertFrom-Json +if ($manifest.name -ne 'com.opengameagent.runtime' -or + $manifest.unity -ne '6000.0' -or + $manifest.unityRelease -ne '0f1' -or + [string]::IsNullOrWhiteSpace($manifest.documentationUrl) -or + [string]::IsNullOrWhiteSpace($manifest.licensesUrl) -or + $manifest.samples.Count -ne 1) { + throw 'Unity store manifest metadata is incomplete.' +} + +$notices = Get-Content -LiteralPath ( + Join-Path $repositoryRoot 'engines\unity\Packages\com.opengameagent.runtime\Third-Party Notices.txt') -Raw +foreach ($assembly in @( + 'Microsoft.Bcl.AsyncInterfaces', + 'System.Buffers', + 'System.Memory', + 'System.Numerics.Vectors', + 'System.Runtime.CompilerServices.Unsafe', + 'System.Text.Encodings.Web', + 'System.Text.Json', + 'System.Threading.Tasks.Extensions')) { + if (-not $notices.Contains($assembly, [StringComparison]::Ordinal)) { + throw "Third-party notices do not identify '$assembly'." + } +} + +$plugin = Get-Content -LiteralPath ( + Join-Path $repositoryRoot 'engines\godot\addons\open_game_agent\plugin.cfg') -Raw +if ($plugin -notmatch '(?m)^script="plugin\.gd"\r?$') { + throw 'Godot add-on must declare a real editor plug-in script.' +} + +$sourceLicenseHash = (Get-FileHash -LiteralPath (Join-Path $repositoryRoot 'LICENSE') -Algorithm SHA256).Hash +$addonLicenseHash = (Get-FileHash -LiteralPath ( + Join-Path $repositoryRoot 'engines\godot\addons\open_game_agent\LICENSE') -Algorithm SHA256).Hash +if ($sourceLicenseHash -ne $addonLicenseHash) { + throw 'The Godot source add-on license must match the repository license.' +} + +$artworkRoot = Join-Path $repositoryRoot 'docs\store-listings\artwork\rendered' +$artwork = @(Get-ChildItem -LiteralPath $artworkRoot -File -Filter '*.png' | Sort-Object Name) +if ($artwork.Count -ne 3) { + throw 'Store listing must contain exactly three rendered PNG assets.' +} + +foreach ($image in $artwork) { + if ($image.Length -gt 10MB) { + throw "Store artwork '$($image.Name)' exceeds 10 MiB." + } + + $bytes = [IO.File]::ReadAllBytes($image.FullName) + if ($bytes.Length -lt 24 -or + $bytes[0] -ne 0x89 -or $bytes[1] -ne 0x50 -or $bytes[2] -ne 0x4E -or $bytes[3] -ne 0x47) { + throw "Store artwork '$($image.Name)' is not a PNG." + } + + $width = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 16)) + $height = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 20)) + if ($width -ne 1600 -or $height -ne 900) { + throw "Store artwork '$($image.Name)' must be 1600x900." + } +} + +foreach ($listing in @('godot.md', 'unity.md')) { + $content = Get-Content -LiteralPath (Join-Path $repositoryRoot "docs\store-listings\$listing") -Raw + if ($content -match '(?i)\b(TBD|TODO|coming soon)\b') { + throw "Store listing '$listing' contains unfinished copy." + } +} + +Write-Output 'Store listing metadata, notices, samples, and artwork are valid.' diff --git a/tools/render-store-artwork.gd b/tools/render-store-artwork.gd new file mode 100644 index 0000000..7532b69 --- /dev/null +++ b/tools/render-store-artwork.gd @@ -0,0 +1,32 @@ +extends SceneTree + + +func _initialize() -> void: + var args := OS.get_cmdline_user_args() + if args.size() != 2: + push_error("Expected source SVG and destination PNG paths.") + quit(2) + return + + var source := args[0] + var destination := args[1] + var content := FileAccess.get_file_as_bytes(source) + if content.is_empty(): + push_error("Could not read SVG: " + source) + quit(3) + return + + var image := Image.new() + var load_error := image.load_svg_from_buffer(content, 1.0) + if load_error != OK: + push_error("Could not render SVG: " + error_string(load_error)) + quit(4) + return + + var save_error := image.save_png(destination) + if save_error != OK: + push_error("Could not save PNG: " + error_string(save_error)) + quit(5) + return + + quit(0)