diff --git a/.github/ISSUE_TEMPLATE/doc_issue.yml b/.github/ISSUE_TEMPLATE/doc_issue.yml index b02aaad..1e3ec08 100644 --- a/.github/ISSUE_TEMPLATE/doc_issue.yml +++ b/.github/ISSUE_TEMPLATE/doc_issue.yml @@ -13,7 +13,7 @@ body: id: project attributes: label: Project - description: Which project's docs this is about (e.g. tsvrc). + description: Which project's docs this is about (e.g. tsvrc or udon-test-kit). validations: required: true - type: dropdown diff --git a/AGENTS.md b/AGENTS.md index 81906c4..2c14abf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,45 +1,20 @@ # AGENTS.md -## Project overview +Docusaurus site (TypeScript, Node 20+) for every project in the `tsvrc` org, one folder per project +under `docs//`. [CONTRIBUTING.md](CONTRIBUTING.md) covers the layout, translations and pull +requests, and [STYLE_GUIDE.md](STYLE_GUIDE.md) covers writing; follow both. This file only adds what +they leave out. -Docusaurus site (TypeScript, Node 20+) documenting every project under the `tsvrc` org, -namespaced under `docs//` rather than one repo per project. See -[CONTRIBUTING.md](CONTRIBUTING.md) for the multi-project layout and the i18n workflow. +## Verify -## Setup - -- `npm install` -- `npm start` for a dev server at `http://localhost:3000`. Content hot-reloads; restart - after changing `docusaurus.config.ts`. - -## Build / verify - -- `npm run build` before calling any change done. `onBrokenLinks: 'throw'` fails the - build on a broken link, catching mistakes `npm start` won't. - -## Code style - -Follow [STYLE_GUIDE.md](STYLE_GUIDE.md) for any `.md`/`.mdx` change: Diátaxis categories, -linking instead of duplicating, avoiding AI-sounding prose. +Run `npm run build` before calling a change done. It builds both locales and fails on a broken +link, which `npm start` doesn't catch. ## Gotchas -- `sidebars.ts` is hand-written per project, not a blanket autogenerated tree. A - `dirName` pointed straight at a leaf folder returns an unlabeled flat list, so a - category's `label` has to come from the wrapping `{type: 'category', ...}` node, not a +- `sidebars.ts` is written by hand. A `dirName` pointed at a leaf folder returns an unlabeled flat + list, so a category's `label` comes from its wrapping `{type: 'category', ...}` node, not a `_category_.json`. -- `tsvrc-core` (code) and this repo (docs) aren't wired together, no submodule, no - build-time fetch, and can drift. Changed public API or behavior in `tsvrc-core`, or - something under `docs/tsvrc/`? Check the other side still matches. - -## Commit / PR rules - -- Every commit must be signed off (`git commit -s`) per the - [DCO](https://developercertificate.org/). -- Disclose AI assistance in the PR description. You're responsible for checking the - result renders correctly, the same as if you'd written it by hand. - -## Security - -- Don't commit secrets or API tokens. Report vulnerabilities per `SECURITY.md`, not as a - public issue. +- Each project's code lives in its own repo (`tsvrc-core`, `udon-test-kit`), and nothing keeps it in + sync with this one. When a project's public behavior changes, check its pages here, and the Core + testing pages too when Udon Test Kit changes. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 858e128..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,6 +0,0 @@ - - -@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ef27b8..d0e51aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,19 @@ duplicating, words/phrases to avoid, sentence shape, formatting rules. Each project under the `tsvrc` org gets its own `docs//` folder here rather than its own docs repo. Adding docs for a new project means adding a folder here and a -matching top-level category in `sidebars.ts`, not creating a new repo. +matching top-level category in `sidebars.ts`, not creating a new repo. Every project shares +one sidebar, so a category whose label another project already uses, such as "Reference", +needs a unique `key`; without one, the Spanish build fails on a duplicate translation key. +`docs/udon-test-kit/` is set up this way. + +A page every project shares, such as [Install a TsVRC package](docs/install-a-package.md), +sits at the root of `docs/` instead. Each project's own install page links to it and adds only +what's specific to that project: the package name, requirements, and how to check that it +worked. + +A project's docs stand on their own. When one project depends on another, as TsVRC does on +Udon Test Kit, the dependent project's pages link to the other's where a reader needs it, and +the project depended on never mentions its consumers. ## Adding a translation @@ -29,20 +41,22 @@ Docs content stays English-only and unversioned under `docs//`. A trans lives in a parallel tree at `i18n//docusaurus-plugin-content-docs/current//`, mirroring the same relative paths and filenames; adding a language never touches the English files. See -`docs/tsvrc/intro.md` and its Spanish counterpart for a worked example to copy. +`docs/core/intro.md` and its Spanish counterpart for a worked example to copy. `src/pages/` translates differently: via `@docusaurus/Translate` in the component source, with strings in `i18n//code.json`. A heading another page links to by anchor needs a stable id that survives translation, -since translating the heading text changes the anchor. Docusaurus's MDX v3 syntax for this +since translating the heading text changes the anchor. The Docusaurus MDX v3 syntax for this is a comment, not `{#id}`: `## Play Mode tests {/* #play-mode-tests */}`. Sidebar category labels live in `sidebars.ts` itself, not a translatable file, so they render in English for -every locale regardless of translated content underneath. Don't override Docusaurus's +every locale regardless of translated content underneath. Don't override the Docusaurus theme chrome (pagination, admonition labels, sidebar buttons) in this site's own -`code.json`; it ships its own translated defaults per locale. +`code.json`; it ships its own translated defaults per locale. The search plugin +(`@easyops-cn/docusaurus-search-local`) ships no Spanish, so its `theme.SearchBar.*` and +`theme.SearchPage.*` strings are the exception and live in `code.json`. Before translating a technical term, check how the target language's own community or -official docs (Unity's localized manual, GitHub's glossary, VRChat's community) actually +official docs (the localized Unity manual, the GitHub glossary, the VRChat community) actually render it rather than translating literally. Some terms stay in English even in translated prose, others have a real idiomatic equivalent that isn't the literal word-for-word choice. This repo's own code identifiers (`_ts.Memory`, `TsStart`) are never translated, including diff --git a/README.md b/README.md index 1f6cc91..fadb4a5 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,10 @@ [![License: CC BY 4.0](https://img.shields.io/badge/license-CC%20BY%204.0-58a6ff?labelColor=161b22)](LICENSE) The [Docusaurus](https://docusaurus.io/) site behind [tsvrc.com](https://tsvrc.com), -documenting every project under the `tsvrc` GitHub org. Framework code lives in -[`tsvrc/tsvrc-core`](https://github.com/tsvrc/tsvrc-core); this repo is docs and content -only. +documenting every project under the `tsvrc` GitHub org. Each project's code lives in its own +repository, such as [`tsvrc/tsvrc-core`](https://github.com/tsvrc/tsvrc-core) for +Core and [`tsvrc/udon-test-kit`](https://github.com/tsvrc/udon-test-kit) for Udon Test +Kit; this repo is docs and content only. ## Development diff --git a/SECURITY.md b/SECURITY.md index 09d1821..be8d482 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Reporting a vulnerability -Don't open a public issue. Use [GitHub's private vulnerability +Don't open a public issue. Use [GitHub private vulnerability reporting](https://github.com/tsvrc/docs/security/advisories/new) for this repository instead, so the report isn't visible before a fix is available. diff --git a/docs/core/add-to-your-project.md b/docs/core/add-to-your-project.md new file mode 100644 index 0000000..f27a073 --- /dev/null +++ b/docs/core/add-to-your-project.md @@ -0,0 +1,42 @@ +--- +id: add-to-your-project +title: Add TsVRC Core to your project +sidebar_position: 1 +--- + +# Add TsVRC Core to your project + +Install TsVRC Core in a VRChat world project. + +## Requirements + +- **Unity 2022.3.** The Core package manifest targets this version specifically. +- **The [VRChat Worlds SDK](https://creators.vrchat.com/worlds/) `3.10.x`.** + +## Install the package + +Install **TsVRC Core** (`com.tsvrc.core`) as described in +[Install a TsVRC package](/docs/install-a-package). Its `.unitypackage` is on +[the Core GitHub releases](https://github.com/tsvrc/tsvrc-core/releases). + +Core depends on [Udon Test Kit](/docs/udon-test-kit/intro) (`com.tsvrc.udon-test-kit`) for its +testing helpers. The Creator Companion installs it along with Core. With the `.unitypackage`, +import [the kit's](/docs/udon-test-kit/add-to-your-project) too, unless your project already has +it. + +You'll know it worked when the Unity menu bar gains a **Tsvrc** menu. Everything Core does +from here runs from there. + +## What you just added + +The Core manifest description sums it up: structured initialization, dependency wiring, +and editor codegen tooling for building VRChat worlds with UdonSharp. Concretely, the +package compiles as one runtime assembly (`Tsvrc.Runtime`), the base classes your world's +scripts extend and call into, and one editor-only assembly (`Tsvrc.Editor`) holding all +the codegen and Configure-window tooling. See [How Core fits +together](./core-concepts/how-it-fits-together) for how those two halves relate. + +## Next + +Continue to [Build your first behaviour](./first-behaviour) to initialize Core in a +scene and get a script running. diff --git a/docs/tsvrc/codegen/config/group-tree-and-inspectors.md b/docs/core/codegen/config/group-tree-and-inspectors.md similarity index 89% rename from docs/tsvrc/codegen/config/group-tree-and-inspectors.md rename to docs/core/codegen/config/group-tree-and-inspectors.md index 6a7bdc8..6228c28 100644 --- a/docs/tsvrc/codegen/config/group-tree-and-inspectors.md +++ b/docs/core/codegen/config/group-tree-and-inspectors.md @@ -20,15 +20,15 @@ group's direct entries on the right (row rendering delegated to with different parameters. `memberPrefix`/`memberSuffix` control whether a live member-name preview is shown at all (`"_ts."` for Globals, `"Create"`/`"(parent)"` for Factories, neither for Pool or Constructs, since neither generates a name at all). `prefixRespectsToggle` -mirrors [`BuildGroupPrefix`](../internals/ts-module)'s own `respectToggle` -parameter: Factory always prefixes regardless of a group's `IncludeInName`, Global respects +mirrors the `respectToggle` parameter of +[`BuildGroupPrefix`](../internals/ts-module): Factory always prefixes regardless of a group's `IncludeInName`, Global respects it per-group. Its `State` class holds purely UI-local state (tree expansion, selection, search text, pagination), never written back to any config asset, persisted across a domain reload via `SessionState`. It's keyed by a combination of the target object's type name and the -property name being drawn, specifically so `TsWindow`'s `TsConfig`-backed trees and -`TsBuiltinConfigInspector`'s `TsBuiltinConfig`-backed trees (which share property names like +property name being drawn, specifically so the `TsConfig`-backed trees in `TsWindow` and the +`TsBuiltinConfig`-backed trees in `TsBuiltinConfigInspector` (which share property names like `"GlobalGroups"`) never collide on the same session key. `Draw` returns whether a drag-and-drop reparent action already flushed its own nested @@ -44,8 +44,8 @@ into: a delete button, and per-row hint text covering three cases a bare `Proper can't communicate on its own. An empty slot means something used to be assigned here and was probably deleted; a scene-instance reference where only a persisted prefab asset is valid (the Pool and Factory tabs, both drawn with `assetsOnly: true`) gets its own hint too; -and a type resolved only via `TsModule.TryResolveObjectType`'s compile-broken fallback is -shown with a note that the compile is currently broken, since Unity's own `PropertyField` +and a type resolved only via the compile-broken fallback in `TsModule.TryResolveObjectType` is +shown with a note that the compile is currently broken, since the Unity `PropertyField` can't display a component's real type while nothing compiles. ## The three "managed by Configure" inspectors diff --git a/docs/tsvrc/codegen/config/linked-scene.md b/docs/core/codegen/config/linked-scene.md similarity index 94% rename from docs/tsvrc/codegen/config/linked-scene.md rename to docs/core/codegen/config/linked-scene.md index 5c31aff..256385a 100644 --- a/docs/tsvrc/codegen/config/linked-scene.md +++ b/docs/core/codegen/config/linked-scene.md @@ -12,7 +12,7 @@ scaffold root. It exists to close a real correctness gap. Without it, every code searched "whatever scene happens to be loaded," which meant switching to look at an unrelated scene, or a play-mode test's own temporary scene, could silently regenerate a project's real generated output with an almost-empty result. A scene with no `TsConfig` -compiles clean, and per `TsModule`'s own snapshot-fallback logic, a clean compile with fewer +compiles clean, and per the snapshot-fallback logic in `TsModule`, a clean compile with fewer entries than before is treated as a deliberate deletion, not an error. ## How it's set @@ -36,7 +36,7 @@ Once a scene is linked, every scene-scoped lookup (`Find`, `FindType`, `FindA `FindAllType`) is scoped strictly to that scene's root objects, rather than searching whatever's currently loaded. `Find`/`FindType` additionally refuse to guess: if more than one match exists within the linked scene, they return `null` rather than picking whichever -one Unity's iteration order happens to return first, the same "don't guess" stance +one the Unity iteration order happens to return first, the same "don't guess" stance `InstanceModule` takes for resolving a project's single scaffold instance. `IsConfiguredButNotLoaded` is true whenever a scene has been linked but isn't currently diff --git a/docs/tsvrc/codegen/config/translation-config.md b/docs/core/codegen/config/translation-config.md similarity index 100% rename from docs/tsvrc/codegen/config/translation-config.md rename to docs/core/codegen/config/translation-config.md diff --git a/docs/tsvrc/codegen/config/ts-config.md b/docs/core/codegen/config/ts-config.md similarity index 94% rename from docs/tsvrc/codegen/config/ts-config.md rename to docs/core/codegen/config/ts-config.md index 4d1ebef..7dcf2ef 100644 --- a/docs/tsvrc/codegen/config/ts-config.md +++ b/docs/core/codegen/config/ts-config.md @@ -48,10 +48,10 @@ two-pass grace period involved. `TsGroup` is one node in an entry group tree, flat, with a `ParentId` reference rather than a nested/recursive structure, so it maps directly onto a tree-view's row model and avoids -Unity's serialization depth limit for nested types. `Id` is assigned once and never reused, +the Unity serialization depth limit for nested types. `Id` is assigned once and never reused, even after deletion. Groups are purely organizational by default; setting `IncludeInName` turns nesting into namespacing, prefixing an opted-in group's own name (and its -opted-in ancestors') onto the generated member name of everything inside it. `_ts.Spawner` +opted-in ancestors) onto the generated member name of everything inside it. `_ts.Spawner` under a plain group stays `_ts.Spawner`, but under a group named "Enemies" with `IncludeInName` set it becomes `_ts.EnemiesSpawner`. Factory groups always prefix regardless of this toggle; Global groups respect it per-group. Construct groups have no @@ -74,7 +74,7 @@ identifier. `TsBuiltinConfig` is a `ScriptableObject`, not a scene component but a package asset, holding the same Global/Pool/Factory entry-and-group shape as `TsConfig`, but for -registrations TsVRC itself ships and wants present regardless of what any individual +registrations Core itself ships and wants present regardless of what any individual project configures. It's merged with each project's own `TsConfig` at generate time, so a library-provided global or pooled prefab shows up in the generated output the same way a project's own entry would, without every project needing to register it by hand. diff --git a/docs/tsvrc/codegen/config/ts-pending-config-edit.md b/docs/core/codegen/config/ts-pending-config-edit.md similarity index 88% rename from docs/tsvrc/codegen/config/ts-pending-config-edit.md rename to docs/core/codegen/config/ts-pending-config-edit.md index 0cdb239..aa1e6b2 100644 --- a/docs/tsvrc/codegen/config/ts-pending-config-edit.md +++ b/docs/core/codegen/config/ts-pending-config-edit.md @@ -7,7 +7,7 @@ sidebar_position: 3 # TsPendingConfigEdit `Tsvrc.Editor.TsPendingConfigEdit` is the batching primitive behind every Apply/Discard -button in TsVRC's editor UI. [`TsWindow`](./ts-window) owns one per `TsConfig` it edits +button in the Core editor UI. [`TsWindow`](./ts-window) owns one per `TsConfig` it edits (plus a second one for the Settings tab's `TsvrcLogger` fields), and `TsBuiltinConfigInspector` owns one for whichever `TsBuiltinConfig` asset is selected. One instance tracks edits to exactly one target object. @@ -16,9 +16,9 @@ instance tracks edits to exactly one target object. Without this, every `SerializedObject.ApplyModifiedProperties()` call, meaning every single field edit including one keystroke into a name field, would immediately trigger a real -regenerate pass through `TsGenerator`'s own `Undo.postprocessModifications` watch. Typing a +regenerate pass through the `Undo.postprocessModifications` watch in `TsGenerator`. Typing a ten-character name would fire ten regenerate passes. `TsPendingConfigEdit` suppresses -`TsGenerator`'s automatic triggers for the duration of an in-progress edit and only lets the +the `TsGenerator` automatic triggers for the duration of an in-progress edit and only lets the accumulated changes trigger one real regenerate when you explicitly click Apply. ## The baseline @@ -39,7 +39,7 @@ GameObject to live on at all) or as a bare `ScriptableObject` instance otherwise Three calls, in order, once per `OnGUI` pass: 1. **`BeginFrame()`** — arms suppression proactively, before any edit could happen this - frame. This matters because some UI code (`TsGroupTreeGUI`'s drag-and-drop reparenting) + frame. This matters because some UI code (the drag-and-drop reparenting in `TsGroupTreeGUI`) calls `ApplyModifiedProperties()` itself, mid-draw, ahead of the caller's own outer call, so suppression has to already be active by then. 2. *(the tab draws its fields, and the caller calls `SerializedObject.ApplyModifiedProperties()` @@ -67,5 +67,5 @@ state immediately. A discarded edit never reaches `ScheduleRerun()`. baseline clone and any held suppression scope. It never has data to lose by the time it runs, since `OnDisable` also fires around an ordinary domain reload, where cleaning up a pending edit would be wrong because the user hasn't decided anything yet, while `OnDestroy` -only fires once the window or inspector is actually closing for good, by which point Unity's -own save-changes dialog has already forced a real Apply or Discard decision. +only fires once the window or inspector is actually closing for good, by which point the Unity +save-changes dialog has already forced a real Apply or Discard decision. diff --git a/docs/tsvrc/codegen/config/ts-window.md b/docs/core/codegen/config/ts-window.md similarity index 92% rename from docs/tsvrc/codegen/config/ts-window.md rename to docs/core/codegen/config/ts-window.md index 0238281..7373308 100644 --- a/docs/tsvrc/codegen/config/ts-window.md +++ b/docs/core/codegen/config/ts-window.md @@ -35,16 +35,16 @@ survives a domain reload via a `[SerializeField]` field: `EditorWindow` instance re-deserialized across a reload, but only serialized fields keep their value across it. The Settings tab isn't a real module: it's a pseudo-module (`SettingsTabModule`) appended -last, because its content spans two different data sources (`LogModule`'s logging fields, -which live on the scene's `TsvrcLogger`, and `TsConfig`'s tree-shaking fields) that don't +last, because its content spans two different data sources (the `LogModule` logging fields, +which live on the scene's `TsvrcLogger`, and the `TsConfig` tree-shaking fields) that don't belong to any single registered module. ## Apply / Discard Every tab's edits are batched behind an explicit Apply/Discard step rather than triggering a real regenerate on every single property change. That batching is the entire reason -[`TsPendingConfigEdit`](./ts-pending-config-edit) exists. `TsWindow` participates in Unity's -own "unsaved changes" contract (`hasUnsavedChanges`, `SaveChanges`, `DiscardChanges`): it +[`TsPendingConfigEdit`](./ts-pending-config-edit) exists. `TsWindow` participates in the Unity +"unsaved changes" contract (`hasUnsavedChanges`, `SaveChanges`, `DiscardChanges`): it decorates the window's title/tab and blocks it from closing, including on an editor quit, until you explicitly apply or discard. `TsWindow` tracks two independent pending-edit sets at once, the active tab's `TsConfig` edits and the Settings tab's separate `TsvrcLogger` @@ -54,7 +54,7 @@ edits, both folded into one shared "you have unapplied changes" footer and one s Beyond the window-close path, switching the linked scene (or picking a different scene to link) also risks discarding an in-progress edit silently. `ResolvePendingChangesBeforeSwitch` guards that specific action with its own Apply/Cancel/Discard dialog, since -`hasUnsavedChanges`'s built-in protection only covers the window actually closing. +the built-in protection of `hasUnsavedChanges` only covers the window actually closing. ## Status reporting diff --git a/docs/tsvrc/codegen/internals/adding-a-codegen-module.md b/docs/core/codegen/internals/adding-a-codegen-module.md similarity index 95% rename from docs/tsvrc/codegen/internals/adding-a-codegen-module.md rename to docs/core/codegen/internals/adding-a-codegen-module.md index 916c6e0..93a1690 100644 --- a/docs/tsvrc/codegen/internals/adding-a-codegen-module.md +++ b/docs/core/codegen/internals/adding-a-codegen-module.md @@ -6,7 +6,7 @@ sidebar_position: 1 # Adding a new codegen module -A how-to guide for contributors extending TsVRC's own generator, not for end users +A how-to guide for contributors extending the Core generator, not for end users configuring a world. It assumes you've read [`TsModule`](./ts-module) and [`TsGenerator`](./ts-generator), and at least a couple of the existing modules under Codegen > Modules, for example [`LogModule`](../modules/log-module). The pattern below is @@ -75,8 +75,8 @@ If your module generates a member accessible via `_ts.Name`, report it through member compile errors, and implement `ExcludeFieldNames(names)` to drop the losing side of a collision cleanly (with a warning explaining the fix, not just silently disappearing). Only reach for `ReservedFieldNamePrecedence` if your module declares a name unconditionally, -regardless of any user config (`InstanceModule`'s `Instance`, `TsSingleComponentModule`'s -`Log`/`Memory`). An ordinary config-driven module should use the default precedence (`0`) +regardless of any user config (`Instance` from `InstanceModule`, `Log`/`Memory` from +`TsSingleComponentModule`). An ordinary config-driven module should use the default precedence (`0`) and let a real naming conflict resolve the normal way. ## Watch the right things diff --git a/docs/tsvrc/codegen/internals/supporting-utilities.md b/docs/core/codegen/internals/supporting-utilities.md similarity index 93% rename from docs/tsvrc/codegen/internals/supporting-utilities.md rename to docs/core/codegen/internals/supporting-utilities.md index 6b8d1fd..6260909 100644 --- a/docs/tsvrc/codegen/internals/supporting-utilities.md +++ b/docs/core/codegen/internals/supporting-utilities.md @@ -7,13 +7,13 @@ sidebar_position: 4 # Generator support utilities A handful of small internal classes back [`TsGenerator`](./ts-generator) and the codegen -modules. None of these are things you call directly as an end user of TsVRC; they're +modules. None of these are things you call directly as an end user of Core; they're documented here for anyone extending the generator itself. ## ScriptIndex Answers "does class X derive from class Y" and "does any script in the project contain this -text" by scanning every `MonoScript`'s raw source text, rather than by reflecting over +text" by scanning the raw source text of every `MonoScript`, rather than by reflecting over compiled types. This matters specifically because it has to keep working **while the project doesn't currently compile**. Reflection over `Assembly-CSharp` is useless exactly when a not-yet-generated member breaks the build, which is one of the more common moments a @@ -26,11 +26,11 @@ passes. ## TsUsageScanner -Layered on top of `ScriptIndex`'s source scan to answer tree-shaking's real question: "is +Layered on top of the `ScriptIndex` source scan to answer tree-shaking's real question: "is this generated member actually used anywhere?" A Global/Log/Memory read has no attribute to reflect over the way `[WirePool]` does. It's just `_ts.Name`, a bare member access in arbitrary source text (`IsMemberReferenced`), so this has to pattern-match source rather -than inspect live objects. A Factory's generated `CreateName(parent)` call site is matched +than inspect live objects. The generated `CreateName(parent)` call site of a Factory is matched the same way, via a separate method-call pattern (`IsMethodCallReferenced`). The matching is deliberately conservative: a false positive (treating something as used when it isn't) only wastes a few bytes of generated code, but a false negative would silently drop real content, diff --git a/docs/tsvrc/codegen/internals/ts-generator.md b/docs/core/codegen/internals/ts-generator.md similarity index 86% rename from docs/tsvrc/codegen/internals/ts-generator.md rename to docs/core/codegen/internals/ts-generator.md index da6debe..df421f9 100644 --- a/docs/tsvrc/codegen/internals/ts-generator.md +++ b/docs/core/codegen/internals/ts-generator.md @@ -19,13 +19,14 @@ Regenerate**, when a watched asset changes, or automatically after every recompi - **A watched asset changes** (`TsAssetWatcher`) — any asset path a module declared via `WatchedAssets()` being imported, deleted, or moved schedules a rerun. A scene hierarchy change a module cares about, or a property edit on a watched component type, schedules one - too, through `TsGenerator`'s own hierarchy-changed and Undo-postprocess hooks rather than + too, through the hierarchy-changed and Undo-postprocess hooks in `TsGenerator` rather than through `TsAssetWatcher` itself. - **Before a VRChat world build** (`TsBuildCompile`) — see below. -Every *automatic* trigger is gated by `AutomaticTriggersSuppressed`, which is true during -any automated test run (detected once, from the process's own `-runTests` command-line -flag) or inside an explicit `SuppressAutomaticTriggers()` scope. This exists because an +Every *automatic* trigger is gated by `AutomaticTriggersSuppressed`, which is true while +Unity Test Framework runs tests, whether from the Test Runner window or a `-runTests` +command-line run, while the Editor is entering or in Play Mode, and inside an explicit +`SuppressAutomaticTriggers()` scope. This exists because an automatic pass reacting to whatever scene happens to be open, a test's own temporary scene in particular, could otherwise silently regenerate a real project's output down to almost nothing (see [`TsLinkedScene`](../config/linked-scene) for the deeper reason this is @@ -37,11 +38,11 @@ otherwise fire unattended are. A pass with `allowBootstrap: false` (every automatic trigger's default) won't create a project's scaffold from nothing. It waits until `HasBootstrapSignal()` finds a real reason -this project uses TsVRC (an existing `TsConfig` in the linked scene, an existing compiled +this project uses Core (an existing `TsConfig` in the linked scene, an existing compiled scaffold instance, or *any* non-abstract `Instance` subclass anywhere in the project, unless marked [`[TsCodegenIgnore]`](../../core-concepts/attributes.md)). Until that signal appears, the generator just re-arms a hierarchy-changed watch and returns. This is what makes an -unrelated, freshly-opened project safe to have TsVRC installed in without it immediately +unrelated, freshly-opened project safe to have Core installed in without it immediately scaffolding files nobody asked for. A deliberate **Force Regenerate**/**Initialize Tsvrc** click passes `allowBootstrap: true` and *is* the signal. @@ -71,19 +72,19 @@ runaway recursion. Real, well-behaved modules converge in one extra pass, since ## Before a build -`TsBuildCompile` (a VRChat SDK build callback, ordered to run before UdonSharp's own build +`TsBuildCompile` (a VRChat SDK build callback, ordered to run before the UdonSharp build pass) forces a generation pass before every scene build, so generated files and scene wiring can't go stale relative to what's about to ship. It checks `TsLinkedScene.IsConfiguredButNotLoaded` *before* `HasBootstrapSignal()` specifically because the bootstrap signal's own any-Instance-subclass-anywhere fallback can't distinguish "the correct scene is open" from "some unrelated scene is open"; only the linked-scene check can catch that. If nothing has ever been set up at all, it offers to cancel the build rather than silently shipping a world -where every `TsvrcBehaviour`'s `_ts` reference would be `null` at runtime. +where the `_ts` reference of every `TsvrcBehaviour` would be `null` at runtime. ## Why this exists, briefly `TsGenerator` is the concrete mechanism behind the codegen-vs-runtime-reflection split -described in [How TsVRC fits together](../../core-concepts/how-it-fits-together): every module's config is +described in [How Core fits together](../../core-concepts/how-it-fits-together): every module's config is read once, at edit time, and baked into real generated C#. Nothing about this pipeline runs in the built world. See that page for the reasoning; this page only covers how the pipeline itself is sequenced and triggered. diff --git a/docs/tsvrc/codegen/internals/ts-module.md b/docs/core/codegen/internals/ts-module.md similarity index 98% rename from docs/tsvrc/codegen/internals/ts-module.md rename to docs/core/codegen/internals/ts-module.md index 54cb3eb..322a29a 100644 --- a/docs/tsvrc/codegen/internals/ts-module.md +++ b/docs/core/codegen/internals/ts-module.md @@ -10,7 +10,7 @@ sidebar_position: 2 [`LogModule`](../modules/log-module), [`MemoryModule`](../modules/memory-module), [`PoolModule`](../modules/pool-module), and the other six modules documented under Codegen > Modules all extend it. This page is for understanding how the generator pipeline -works and, eventually, for writing a new module yourself; if you're only *using* TsVRC +works and, eventually, for writing a new module yourself; if you're only *using* Core rather than extending its generator, you can skip it. See [Adding a codegen module](./adding-a-codegen-module) for a worked example of implementing one. @@ -47,7 +47,7 @@ modules want the same name: the higher-precedence module keeps it, others drop i exact tie strips the name from every claimant, since a tie at the same precedence is a genuine, unresolvable collision. `ReservedFieldNamePrecedence` (`int.MaxValue`) marks a name that's unconditionally present in generated code regardless of any user config, for example -`InstanceModule`'s own `Instance` property, so a config entry that happens to auto-derive +the `Instance` property of `InstanceModule`, so a config entry that happens to auto-derive the same name is caught as an exclusion instead of silently producing a duplicate-member compile error. diff --git a/docs/tsvrc/codegen/internals/ts-translation-window.md b/docs/core/codegen/internals/ts-translation-window.md similarity index 100% rename from docs/tsvrc/codegen/internals/ts-translation-window.md rename to docs/core/codegen/internals/ts-translation-window.md diff --git a/docs/tsvrc/codegen/modules/construct-module.md b/docs/core/codegen/modules/construct-module.md similarity index 100% rename from docs/tsvrc/codegen/modules/construct-module.md rename to docs/core/codegen/modules/construct-module.md diff --git a/docs/tsvrc/codegen/modules/factory-module.md b/docs/core/codegen/modules/factory-module.md similarity index 97% rename from docs/tsvrc/codegen/modules/factory-module.md rename to docs/core/codegen/modules/factory-module.md index 4809edb..525d26b 100644 --- a/docs/tsvrc/codegen/modules/factory-module.md +++ b/docs/core/codegen/modules/factory-module.md @@ -37,7 +37,7 @@ namespace. The same prefab registered under two differently-named groups (for example, both an "Enemies" group and a "Traps" group spawning the same projectile prefab) is a legitimate, -common case, not a mistake. Unlike Pool's duplicate-reference detection, `FactoryModule` +common case, not a mistake. Unlike the Pool duplicate-reference detection, `FactoryModule` deliberately does *not* flag a prefab appearing more than once across different registrations. diff --git a/docs/tsvrc/codegen/modules/global-module.md b/docs/core/codegen/modules/global-module.md similarity index 94% rename from docs/tsvrc/codegen/modules/global-module.md rename to docs/core/codegen/modules/global-module.md index f24fb87..3840dab 100644 --- a/docs/tsvrc/codegen/modules/global-module.md +++ b/docs/core/codegen/modules/global-module.md @@ -31,7 +31,7 @@ An entry's generated member name is its explicit `Name` if set, otherwise derive literal type name `"GameObject"` would be useless and collide across every ungrouped plain GameObject entry), an `Animator` reference is named `{GameObjectName}Animator`, and anything else uses its component type name directly. A group with `IncludeInName` set prefixes the -final name with its (and its opted-in ancestors') own name, exactly as described on +final name with its (and its opted-in ancestors) own name, exactly as described on [`TsConfig`](../config/ts-config). ## Generated shape @@ -56,7 +56,7 @@ what a real Global entry usually is. `ExcludeFieldNames` drops any Global entry whose name lost a cross-module collision, most commonly two Global entries independently deriving the same name (two plain GameObjects both named "Manager", say), or a Global entry that happens to auto-derive a reserved name -like `Instance` (see [`InstanceModule`](./instance-module)'s reserved-name precedence). It +like `Instance` (see the reserved-name precedence in [`InstanceModule`](./instance-module)). It logs a warning naming the fix: give one of the conflicting entries a distinct explicit `Name`. A Construct entry never causes this, since a construct's field is always private and never claims a name at all. diff --git a/docs/tsvrc/codegen/modules/instance-module.md b/docs/core/codegen/modules/instance-module.md similarity index 88% rename from docs/tsvrc/codegen/modules/instance-module.md rename to docs/core/codegen/modules/instance-module.md index fd78dbf..1496f45 100644 --- a/docs/tsvrc/codegen/modules/instance-module.md +++ b/docs/core/codegen/modules/instance-module.md @@ -17,7 +17,7 @@ action needed. `DetectInstanceType` reflects across every loaded assembly, filters out anything marked [`[TsCodegenIgnore]`](../../core-concepts/attributes.md) (test doubles, most commonly), and -deduplicates by full type name. Unity's `AppDomain` can carry stale duplicate copies of the +deduplicates by full type name. The Unity `AppDomain` can carry stale duplicate copies of the same assembly across successive recompiles, which would otherwise make one real subclass look ambiguous just because it was seen twice. Zero candidates or exactly one are both unambiguous outcomes; more than one logs an error and makes `Wire()` leave whatever's @@ -27,11 +27,11 @@ possibly-transient subclass momentarily appeared would be worse than doing nothi ## Surviving a broken compile The generated field and property (`_instance` / `public override Instance Instance => -_instance;`) are always declared as the plain `Instance` base type. `GenerateCode()`'s output +_instance;`) are always declared as the plain `Instance` base type. The output of `GenerateCode()` never depends on which concrete subclass was detected, so a broken compile can't corrupt the -generated *code*. What a broken compile *can* corrupt is `Wire()`'s live-reflection -detection: if the AppDomain scan finds zero candidates, but `ScriptIndex`'s independent, -source-text-based scan confirms exactly one real `Instance` subclass genuinely exists in +generated *code*. What a broken compile *can* corrupt is the live-reflection +detection in `Wire()`: if the AppDomain scan finds zero candidates, but the independent, +source-text-based `ScriptIndex` scan confirms exactly one real `Instance` subclass genuinely exists in project source, that almost always means the subclass itself just failed to compile along with everything else. In that case (`_knownViaScriptIndexOnly`), `Wire()` deliberately does nothing rather than destroying the real, working `"Instance"` child object and nulling the diff --git a/docs/tsvrc/codegen/modules/log-module.md b/docs/core/codegen/modules/log-module.md similarity index 89% rename from docs/tsvrc/codegen/modules/log-module.md rename to docs/core/codegen/modules/log-module.md index 5af9fb8..76e46aa 100644 --- a/docs/tsvrc/codegen/modules/log-module.md +++ b/docs/core/codegen/modules/log-module.md @@ -27,8 +27,8 @@ LogWarning("Slot already occupied, skipping"); - **Extra usage detection.** A script rarely writes `_ts.Log` directly: it calls `TsvrcBehaviour.LogInfo`/`LogWarning`/`LogError`, which reach the logger indirectly. Those - wrapper calls never textually mention `_ts.Log`, so `TsSingleComponentModule`'s base - member-access check alone would miss the single most common way logging actually gets + wrapper calls never textually mention `_ts.Log`, so the base member-access check in + `TsSingleComponentModule` alone would miss the single most common way logging actually gets used. `LogModule` reports those three method names as additional usage signatures so tree-shaking doesn't wrongly conclude an actively-used logger is dead. - **No standalone Configure tab.** Logging settings are rendered as a section inside the diff --git a/docs/tsvrc/codegen/modules/memory-module.md b/docs/core/codegen/modules/memory-module.md similarity index 100% rename from docs/tsvrc/codegen/modules/memory-module.md rename to docs/core/codegen/modules/memory-module.md diff --git a/docs/tsvrc/codegen/modules/pool-module.md b/docs/core/codegen/modules/pool-module.md similarity index 100% rename from docs/tsvrc/codegen/modules/pool-module.md rename to docs/core/codegen/modules/pool-module.md diff --git a/docs/tsvrc/codegen/modules/scaffold-module.md b/docs/core/codegen/modules/scaffold-module.md similarity index 93% rename from docs/tsvrc/codegen/modules/scaffold-module.md rename to docs/core/codegen/modules/scaffold-module.md index 25864ea..3bb23cc 100644 --- a/docs/tsvrc/codegen/modules/scaffold-module.md +++ b/docs/core/codegen/modules/scaffold-module.md @@ -45,8 +45,8 @@ own. ## Deterministic program asset field order -`NormalizeProgramAsset` sorts an `UdonSharpProgramAsset`'s internal field-definition -dictionary into a stable order after every pass. Unity/UdonSharp's own serialization of that +`NormalizeProgramAsset` sorts the internal field-definition dictionary of an +`UdonSharpProgramAsset` into a stable order after every pass. The Unity/UdonSharp serialization of that dictionary isn't guaranteed to stay in the same order across compilations, which without this normalization would produce a spurious file diff on every single domain reload even when nothing about the actual fields changed. That's noisy for version control and confusing diff --git a/docs/tsvrc/codegen/modules/translation-module.md b/docs/core/codegen/modules/translation-module.md similarity index 93% rename from docs/tsvrc/codegen/modules/translation-module.md rename to docs/core/codegen/modules/translation-module.md index cb9b857..83da748 100644 --- a/docs/tsvrc/codegen/modules/translation-module.md +++ b/docs/core/codegen/modules/translation-module.md @@ -51,8 +51,8 @@ on the next pass with no manual re-registration needed. listener registered via `SubscribeLanguageChanged`. - **`SubscribeLanguageChanged(UdonSharpBehaviour listener, string callback)`** — registers `listener` to receive `SendCustomEvent(callback)` on every future `SetLanguage` call, its - own small generated pub/sub separate from the base `TsvrcBehaviour`'s `TsSubscribe`/ - `TsEmit`. Subscribing the same listener/callback pair twice is a no-op rather than a + own small generated pub/sub separate from `TsSubscribe`/ + `TsEmit` in the base `TsvrcBehaviour`. Subscribing the same listener/callback pair twice is a no-op rather than a duplicate delivery. - **`Translate(string key)`** / **`Translate(string key, string param)`** — linear-scans the active language's keys for an exact match, returning the untranslated key itself as a @@ -68,5 +68,5 @@ on the next pass with no manual re-registration needed. `AfterFilesStable`/`OnSceneHierarchyChanged` both re-scan for matching `TextMeshProUGUI` targets and compare against the last known set (`SyncEffectiveKeys`). A scene edit that adds or renames a matching text object triggers a regenerate on its own, independent of whether -any language file itself changed, since `Wire()`'s `_translationTargets` array needs to +any language file itself changed, since the `_translationTargets` array in `Wire()` needs to include it. diff --git a/docs/tsvrc/codegen/modules/ts-single-component-module.md b/docs/core/codegen/modules/ts-single-component-module.md similarity index 90% rename from docs/tsvrc/codegen/modules/ts-single-component-module.md rename to docs/core/codegen/modules/ts-single-component-module.md index 957c74d..db01455 100644 --- a/docs/tsvrc/codegen/modules/ts-single-component-module.md +++ b/docs/core/codegen/modules/ts-single-component-module.md @@ -23,10 +23,10 @@ being generated at all. Usage (tracked via the shared `ApplyTreeShaking` mechani this single subsystem as a one-element list) gates exactly two things: whether `GenerateCode()` emits the real field/property/start-method or a stub, and whether the backing scene child object exists at all. A caller reading `_ts.Log` or `_ts.Memory` while -genuinely unused simply gets `TsRoot`'s own `null` default. There's no special-casing needed +genuinely unused simply gets the `null` default from `TsRoot`. There's no special-casing needed elsewhere in the framework for "this subsystem happens to be turned off." -`IsUsed` is exposed as a `protected` property specifically so `LogModule`'s own Settings-tab +`IsUsed` is exposed as a `protected` property specifically so the `LogModule` Settings-tab integration can tell "never generated yet, click Force Regenerate" apart from "deliberately tree-shaken away, won't reappear until referenced or force-included." Without it, the tab's guidance would be actively misleading once tree-shaking legitimately turns this subsystem @@ -35,7 +35,7 @@ off. ## Self-healing, but never destructive to the shared package asset `AfterFilesStable()` always calls `ScaffoldModule.EnsureUdonSharpProgramAsset` for -`ComponentType`'s own script/asset pair, regardless of current usage. Those are permanent +the script/asset pair of `ComponentType`, regardless of current usage. Those are permanent package resources (`TsvrcLogger.cs`/`.asset`, `TsvrcMemory.cs`/`.asset`), never created or deleted by this module, only healed if something about their linkage drifted. What usage *does* control is the scene-local child object: when unused, an existing child under the diff --git a/docs/tsvrc/core-concepts/attributes.md b/docs/core/core-concepts/attributes.md similarity index 95% rename from docs/tsvrc/core-concepts/attributes.md rename to docs/core/core-concepts/attributes.md index bed0adb..21dacdc 100644 --- a/docs/tsvrc/core-concepts/attributes.md +++ b/docs/core/core-concepts/attributes.md @@ -6,7 +6,7 @@ sidebar_position: 6 # Framework attributes -Reference for the three plain marker attributes TsVRC's codegen reads by reflection at +Reference for the three plain marker attributes the Core codegen reads by reflection at edit time. None of them do anything at runtime themselves. They exist so the generator can find and validate things without hardcoding a list of types. @@ -37,7 +37,7 @@ type against its own configuration and validates the pairing at generation time, not compile time, so a mismatched or unregistered pool type surfaces as a codegen warning, not a C# compiler error. See that module's own reference page for exactly what gets checked. If the pooled type is a -`TsvrcBehaviour` subclass, TsVRC calls `TsConstruct` on each pool slot at scene start; +`TsvrcBehaviour` subclass, Core calls `TsConstruct` on each pool slot at scene start; for any other type, the field is just wired to a reference with no construction step. A `[WirePool]` field is read-only in the inspector: `WirePoolAttributeDrawer` disables @@ -73,7 +73,7 @@ public class FakeInstanceForTests : Instance Put `[TsWorldExtensionPoint("GeneratedName")]` on a framework base class to mark it as one world scripts are meant to subclass directly, and to name the generated shadow class that [`ScaffoldModule`](../codegen/modules/scaffold-module) (the codegen module you -already met in [How TsVRC fits together](./how-it-fits-together)) writes for it. See +already met in [How Core fits together](./how-it-fits-together)) writes for it. See that page again for why the shadow class exists at all. In short, it retypes the inherited `_ts` reference to your project's own concrete generated root, so your code gets it fully typed with no cast. @@ -123,7 +123,7 @@ Tagging it would just generate an unused shadow class. The internal class name and its generated shadow name deliberately differ in most cases: `TsvrcBehaviour` keeps its distinguishing `Tsvrc` prefix internally because a bare -`Behaviour` would collide with Unity's own +`Behaviour` would collide with [`UnityEngine.Behaviour`](https://docs.unity3d.com/ScriptReference/Behaviour.html), while the shadow gets the clean name (`TsBehaviour`) that your own code actually extends. diff --git a/docs/tsvrc/core-concepts/how-it-fits-together.md b/docs/core/core-concepts/how-it-fits-together.md similarity index 92% rename from docs/tsvrc/core-concepts/how-it-fits-together.md rename to docs/core/core-concepts/how-it-fits-together.md index 4630499..4190da6 100644 --- a/docs/tsvrc/core-concepts/how-it-fits-together.md +++ b/docs/core/core-concepts/how-it-fits-together.md @@ -1,12 +1,12 @@ --- id: how-it-fits-together -title: How TsVRC fits together +title: How Core fits together sidebar_position: 1 --- -# How TsVRC fits together +# How Core fits together -TsVRC splits into two halves: one lives in the Unity editor while you build, the other +Core splits into two halves: one lives in the Unity editor while you build, the other runs at runtime for players. This page explains how they relate, and how the pieces from [the first-behaviour tutorial](../first-behaviour) fit together. Read it once: every module's reference page assumes you already have this picture instead of re-explaining @@ -40,12 +40,12 @@ given type is currently registered, and giving you a typed reference to your pro own subclass instead of a generic base type. The usual tools for that are runtime reflection or a dependency injection container. -TsVRC solves both while you're still in the editor instead. The codegen pass reads your +Core solves both while you're still in the editor instead. The codegen pass reads your project's types and configuration once, at edit time, and writes plain code with direct references already baked in. Nothing gets looked up while the world is actually running. Whether UdonSharp specifically requires this approach is a separate question this page -doesn't try to answer. What's certain: TsVRC generates every extension point instead of +doesn't try to answer. What's certain: Core generates every extension point instead of resolving it at runtime. That's why your own scripts extend a generated shadow class (`TsBehaviour`, `TsInstance`, and so on) instead of the framework class directly: the shadow is what carries your project's own concrete types. diff --git a/docs/tsvrc/core-concepts/instance.md b/docs/core/core-concepts/instance.md similarity index 89% rename from docs/tsvrc/core-concepts/instance.md rename to docs/core/core-concepts/instance.md index 7cc7319..8e37dd7 100644 --- a/docs/tsvrc/core-concepts/instance.md +++ b/docs/core/core-concepts/instance.md @@ -17,8 +17,8 @@ project; see that module's own reference page for how it's found. ## Usage Override `OnInstanceStart` for setup that should run once the instance itself is ready, -and `IsTsMaster` only if the project needs a notion of "host" that differs from VRChat's -own instance master: +and `IsTsMaster` only if the project needs a notion of "host" that differs from the VRChat +instance master: ```csharp public class GameInstance : TsInstance @@ -32,12 +32,12 @@ public class GameInstance : TsInstance ## Public surface -- **`IsTsMaster`** — a `virtual bool`, currently a thin wrapper over VRChat's own +- **`IsTsMaster`** — a `virtual bool`, currently a thin wrapper over the VRChat [`Networking.IsMaster`](https://udonsharp.docs.vrchat.com/vrchat-api/#networking). It's a real, working default today, not a stub that throws, so a project with no special master logic doesn't need to touch it. Treat the name and the logic behind it as a - placeholder, though: TsVRC's plan is real master management of its own, independent of - VRChat's instance master, and this property will change to reflect that before 1.0. + placeholder, though: the plan for Core is real master management of its own, independent + of the VRChat instance master, and this property will change to reflect that before 1.0. Override it now if your world needs different master logic in the meantime, but expect to revisit that override later. Before gating any world feature on master status at all, note that [VRChat itself recommends against @@ -61,7 +61,7 @@ its own. ## Edge cases worth knowing -`IsTsMaster`'s default implementation reads live VRChat networking state. Call it outside +The default `IsTsMaster` implementation reads live VRChat networking state. Call it outside a real running instance, say directly in an editor test with no network context, and it won't throw. It also won't mean anything: the property is safe to call in a non-runtime environment, just not meaningful there. diff --git a/docs/tsvrc/core-concepts/process.md b/docs/core/core-concepts/process.md similarity index 98% rename from docs/tsvrc/core-concepts/process.md rename to docs/core/core-concepts/process.md index d044298..b74b58b 100644 --- a/docs/tsvrc/core-concepts/process.md +++ b/docs/core/core-concepts/process.md @@ -9,7 +9,7 @@ sidebar_position: 4 `Tsvrc.Core.Process` is the base class for networked, owner-driven processes: something with a start/stop/complete lifecycle that one player (the owner) drives, that survives the owner leaving, and that optionally ticks on an interval. Its generated shadow is -`TsProcess`. It's the root of TsVRC's longest inheritance chain: `PlayerTracker`, +`TsProcess`. It's the root of the longest inheritance chain in Core: `PlayerTracker`, `AutoPlayerTracker`, `ReadyCheckProcess`, and the entire `DataTransferer` chain all build on top of it, so its ownership and lifecycle model is worth understanding well before those pages. @@ -82,7 +82,7 @@ A process has exactly one owner at a time, tracked two ways: a display-name-base `IsProcessOwner()` actually does, since it avoids a string allocation. `StopProcess`/`CompleteProcess` treat two things as sufficient authority to act locally: -being the recognized process owner, or simply holding VRChat's own [object +being the recognized process owner, or simply holding VRChat [object ownership](https://creators.vrchat.com/worlds/udon/networking/ownership/) of the GameObject (`Networking.IsOwner`, layered on top of Unity by the VRChat SDK, not a Unity feature itself). That second check exists for a specific timing window: right @@ -154,7 +154,7 @@ rate-limited to one call per second. Two things worth knowing before relying on directly: - VRChat has no way to restrict who calls a `[NetworkCallable]` method: any player can - invoke these directly. The owner-or-object-owner guard is the only check TsVRC + invoke these directly. The owner-or-object-owner guard is the only check Core applies; any further authorization is the subclass's responsibility. - There's no per-run generation token. Calls beyond the rate limit are [queued, not dropped](https://creators.vrchat.com/worlds/udon/networking/events/#rate-limiting), diff --git a/docs/tsvrc/core-concepts/ts-root.md b/docs/core/core-concepts/ts-root.md similarity index 94% rename from docs/tsvrc/core-concepts/ts-root.md rename to docs/core/core-concepts/ts-root.md index ed7a10d..dfa483f 100644 --- a/docs/tsvrc/core-concepts/ts-root.md +++ b/docs/core/core-concepts/ts-root.md @@ -7,7 +7,7 @@ sidebar_position: 5 # TsRoot `Tsvrc.Core.Generated.TsRoot` is the abstract root every `TsvrcBehaviour` reaches through -its `_ts` field. See [How TsVRC fits together](./how-it-fits-together) for the overall +its `_ts` field. See [How Core fits together](./how-it-fits-together) for the overall picture. This page is its full reference. ## Usage @@ -42,7 +42,7 @@ it never needs to know about a generated class that hasn't been written. A generator module called `ScaffoldModule` writes that real, concrete subclass for your project. It's the type your `TsBehaviour`-extending scripts actually get back once -TsVRC retypes `_ts` for them (see [How TsVRC fits +Core retypes `_ts` for them (see [How Core fits together](./how-it-fits-together#why-a-generation-step-exists-at-all) for how that retyping works). diff --git a/docs/tsvrc/core-concepts/tsvrc-behaviour.md b/docs/core/core-concepts/tsvrc-behaviour.md similarity index 95% rename from docs/tsvrc/core-concepts/tsvrc-behaviour.md rename to docs/core/core-concepts/tsvrc-behaviour.md index 85d2938..d7b3c83 100644 --- a/docs/tsvrc/core-concepts/tsvrc-behaviour.md +++ b/docs/core/core-concepts/tsvrc-behaviour.md @@ -6,11 +6,11 @@ sidebar_position: 2 # TsvrcBehaviour -`Tsvrc.Core.TsvrcBehaviour` is the base class every other TsVRC runtime type extends, +`Tsvrc.Core.TsvrcBehaviour` is the base class every other Core runtime type extends, directly or indirectly. It's an [`UdonSharpBehaviour`](https://udonsharp.docs.vrchat.com/) with structured, one-time initialization and a small publish/subscribe event system built in. Its generated shadow -class is `TsBehaviour`. See [How TsVRC fits together](./how-it-fits-together) for why +class is `TsBehaviour`. See [How Core fits together](./how-it-fits-together) for why your own scripts extend the shadow rather than this class directly. ## Usage @@ -123,7 +123,7 @@ Two failure modes matter here, and they're handled differently on purpose: still gets called normally before that throw happens. - A listener whose GameObject got destroyed *after* subscribing is treated differently: `TsEmit` just skips it, and every other listener still fires. This works because - Unity's [`==` operator](https://docs.unity3d.com/ScriptReference/Object-operator_eq.html) + the Unity [`==` operator](https://docs.unity3d.com/ScriptReference/Object-operator_eq.html) treats a destroyed object as equal to `null`, but `TsEmit` casts to `object` first to check whether the reference is genuinely null (never assigned) versus just destroyed, and only throws for the first case. @@ -143,7 +143,7 @@ work. Two things specific to calling it from here: `GetUdonTypeName()`. You never pass the tag yourself. - `IsTsvrcInternal` (`false` by default) decides which of a message's paired toggles applies: the "Tsvrc Internal" one or the "Your World" one. Only override it to `true` - if you're writing a framework base class that should be classified as part of TsVRC + if you're writing a framework base class that should be classified as part of Core itself. Every subclass of a class that overrides it inherits that classification automatically. Leave it alone in ordinary world scripts. @@ -156,7 +156,7 @@ logging is configured elsewhere. ## Destruction -`TsDestroy()` is `virtual` and defaults to calling Unity's own +`TsDestroy()` is `virtual` and defaults to calling the Unity [`Destroy(gameObject)`](https://docs.unity3d.com/ScriptReference/Object.Destroy.html). In play mode, that defers the actual removal to the end of the current frame rather than happening instantly. Outside play mode, in the editor, Unity refuses to run it diff --git a/docs/tsvrc/explanations/builtin-registrations.md b/docs/core/explanations/builtin-registrations.md similarity index 89% rename from docs/tsvrc/explanations/builtin-registrations.md rename to docs/core/explanations/builtin-registrations.md index 0406b78..f64d81d 100644 --- a/docs/tsvrc/explanations/builtin-registrations.md +++ b/docs/core/explanations/builtin-registrations.md @@ -6,7 +6,7 @@ sidebar_position: 4 # Decision note: builtin pool and factory entries -Eight of TsVRC's own runtime types ship pre-registered in `TsBuiltinConfig`, the +Eight of the Core runtime types ship pre-registered in `TsBuiltinConfig`, the library-wide counterpart to a project's own [`TsConfig`](../codegen/config/ts-config): [`DataTransferer`](../networking-data/data-transfer/data-transferer), [`TsvrcTimer`](../networking-data/tsvrc-timer), @@ -18,18 +18,18 @@ library-wide counterpart to a project's own [`TsConfig`](../codegen/config/ts-co [`RankedGameSession`](../game-flow/ranked-game-session) as a pool entry too, not just the sub-behaviours it composes. A `[WirePool]` field or `_ts.CreateX()` call for any of these eight works the moment you write it, with nothing to add on the Configure window's Pools -or Factories tab first. This note explains why TsVRC ships them pre-registered, and why +or Factories tab first. This note explains why Core ships them pre-registered, and why each one landed on Pool or Factory specifically. ## What shipping them pre-registered actually buys -`TsBuiltinConfig`'s own doc comment describes it plainly: "library-wide Globals/Pool/Factory +The doc comment on `TsBuiltinConfig` describes it plainly: "library-wide Globals/Pool/Factory registrations shipped with tsvrc itself, merged with each world's own `TsConfig` at generate time." The observable effect of that merge is what matters here: without it, using `PlayerTracker` in your own code would need the same round trip registering one of your own project's types does, open Configure, add it to the Pools tab, then come back to write the `[WirePool]` field, even though every project that uses `PlayerTracker` needs it registered -the exact same way, with no project-specific choice to make. Merging TsVRC's own +the exact same way, with no project-specific choice to make. Merging the Core registrations in ahead of a project's own removes that step for these eight types specifically, without changing anything about how a project registers its *own* types. @@ -47,7 +47,7 @@ the two groups. The real split comes from how each generator produces an instanc - **A factory method clones its prefab at the moment you call it**, via an actual `Instantiate()` inside the generated `CreateX(parent)` method. See [the pooling pattern](./pooling-pattern) for why that specific operation, a runtime `Instantiate` - call, is exactly what VRChat's own networking model can't synchronize. That's a hard + call, is exactly what the VRChat networking model can't synchronize. That's a hard constraint on every factory-created object, not a convention: no factory-created instance could correctly participate in networked gameplay regardless of what it is, which is exactly why [`FactoryModule`](../codegen/modules/factory-module) documents that @@ -58,7 +58,7 @@ the two groups. The real split comes from how each generator produces an instanc That difference also explains why `PlayerTracker`, `ReadyCheckProcess`, `AutoPlayerTracker`, `TsvrcTimer`, `DataTransferer`, and `RankedGameSession` couldn't have been factory entries instead, independent of whether they sync. `RankedGameSession` and -several of the others declare their own `[WirePool]` fields (`RankedGameSession`'s +several of the others declare their own `[WirePool]` fields (in `RankedGameSession`, `_lobbyTracker`, `_readyCheck`, and so on), and `PoolModule.Wire()` only ever wires `[WirePool]` fields on objects that already exist in the scene when it runs, at edit time. A factory-cloned instance is created later, during actual gameplay, long after that pass diff --git a/docs/tsvrc/explanations/codegen-vs-reflection.md b/docs/core/explanations/codegen-vs-reflection.md similarity index 82% rename from docs/tsvrc/explanations/codegen-vs-reflection.md rename to docs/core/explanations/codegen-vs-reflection.md index 3d223e8..d99c8e2 100644 --- a/docs/tsvrc/explanations/codegen-vs-reflection.md +++ b/docs/core/explanations/codegen-vs-reflection.md @@ -6,36 +6,36 @@ sidebar_position: 1 # Decision note: codegen instead of runtime reflection -TsVRC resolves a project's own concrete types (which class is your `Instance` subclass, +Core resolves a project's own concrete types (which class is your `Instance` subclass, which behaviours are registered as Constructs, which prefabs are pooled) once, at edit time, and bakes the result into generated C# with direct field references and typed accessors. A framework built the same way in ordinary C# would more commonly reach for runtime reflection or a dependency-injection container to do this lookup dynamically, -lazily, whenever it's actually needed. TsVRC doesn't have that option, for a concrete, +lazily, whenever it's actually needed. Core doesn't have that option, for a concrete, checkable reason: the runtime environment it targets doesn't support it. ## What Udon-compiled code can't do UdonSharp compiles against a fixed set of APIs the Udon VM exposes ("externs"), determined by walking the .NET type system at compile time, not at the compiled program's own runtime. -[VRChat's own documentation of the Udon VM](https://creators.vrchat.com/worlds/udon/vm-and-assembly/#overview-of-the-udon-vm) +[The VRChat documentation of the Udon VM](https://creators.vrchat.com/worlds/udon/vm-and-assembly/#overview-of-the-udon-vm) states this directly: "reflection is not used to access functions," though the VM is "'styled' like it is," meaning a function call compiles down to a fixed extern name resolved ahead of time, not a live, `System.Reflection`-style lookup by member name at runtime. (The -reflection calls you'll find inside UdonSharp's own tooling, for example [the Editor-side +reflection calls you'll find inside the UdonSharp tooling, for example [the Editor-side "proxy" object every `UdonSharpBehaviour` -has](https://udonsharp.docs.vrchat.com/editor-scripting/#overview), run in the Unity -Editor's own C# context while compiling or inspecting a script; they never execute as part +has](https://udonsharp.docs.vrchat.com/editor-scripting/#overview), run in the C# context of the +Unity Editor while compiling or inspecting a script; they never execute as part of the compiled Udon bytecode itself.) -Generics compound the problem: [UdonSharp's own +Generics compound the problem: [the UdonSharp FAQ](https://udonsharp.docs.vrchat.com/frequently-asked-questions/#im-starting-from-scratch-and-need-to-use-c-tutorials-what-common-aspects-of-c-dont-work-in-udonsharp) lists generic classes and generic methods among the C# features that don't work in Udon. A typical reflection-or-DI-based framework leans on both together: reflect over a type, then resolve or construct it generically. With neither tool available in compiled Udon code, that whole approach is closed off, not just inconvenient. -Object creation is restricted the same way: the only instantiation method [VRChat's own API +Object creation is restricted the same way: the only instantiation method [the VRChat API reference](https://udonsharp.docs.vrchat.com/vrchat-api/#vrcinstantiate) lists is `GameObject VRCInstantiate(GameObject original)`, a single, non-generic overload. Since generic methods don't work in Udon at all (the same restriction cited above), there's no @@ -46,10 +46,10 @@ the same limitation. That's part of why generated factory code (see and emits a concrete, non-generic `GetComponent()` call rather than anything parameterized. -## What TsVRC does instead +## What Core does instead None of that machinery exists in compiled Udon, but all of it is perfectly ordinary, -supported C# in the Unity Editor, where TsVRC's own reflection happens. Scanning loaded +supported C# in the Unity Editor, where Core does its own reflection. Scanning loaded assemblies for an `Instance` subclass, or a class tagged [`[TsWorldExtensionPoint]`](../core-concepts/attributes.md), runs entirely in the editor's own C# runtime, at generate time, never inside a compiled UdonSharpBehaviour. The generator diff --git a/docs/tsvrc/explanations/pooling-pattern.md b/docs/core/explanations/pooling-pattern.md similarity index 80% rename from docs/tsvrc/explanations/pooling-pattern.md rename to docs/core/explanations/pooling-pattern.md index 45c55e4..b97d276 100644 --- a/docs/tsvrc/explanations/pooling-pattern.md +++ b/docs/core/explanations/pooling-pattern.md @@ -10,39 +10,39 @@ sidebar_position: 2 compute how many instances of each poolable prefab a project needs, and instantiate exactly that many into the scene as generated content: at edit time, wired and saved as part of the scene, rather than instantiated on demand while the world is running. This mirrors a -constraint documented directly in VRChat's own creator documentation, not an arbitrary design +constraint documented directly in the VRChat creator documentation, not an arbitrary design preference. ## The documented constraint -[UdonSharp's own networking +[The UdonSharp networking guidance](https://udonsharp.docs.vrchat.com/networking-tips-&-tricks/) states plainly, under its "Instantiation" heading, that "instantiated objects cannot be correctly synchronized," and that "the only workaround is to use object-pooling": pre-placing a fixed set of objects in the scene ahead of time and toggling their active state at runtime instead of creating -new ones. [VRChat's own `VRCObjectPool` +new ones. [The VRChat `VRCObjectPool` component](https://udonsharp.docs.vrchat.com/vrchat-api/#vrcobjectpool) documents doing exactly that: it "manage[s] and synchronize[s] the active state of each object it holds," handing one out via a spawn call and returning it later rather than ever instantiating or destroying anything at runtime. That's the same model `PoolModule` follows for `[WirePool]` fields. -## What this means for TsVRC's own design +## What this means for the design of Core Because a networked, poolable object has to already exist in the scene before the world starts, "how many instances do we need" has to be answered before runtime, not lazily as demand appears. There's no safe way to spin up a new synced instance of a type on demand -once players are already in the instance. `PoolModule`'s slot-count computation (see its own +once players are already in the instance. The `PoolModule` slot-count computation (see its own reference page for the dependency-graph math) exists to answer that question as accurately as possible at edit time, from real usage found in the project: how many places actually declare a `[WirePool]` field for a given type, including other pooled types that themselves depend on further pooled types. This page deliberately doesn't go further and explain the lower-level mechanism, for -example exactly why VRChat's networking model ties an object's ability to be owned and +example exactly why the VRChat networking model ties an object's ability to be owned and synced to it having existed since before the scene loaded. That's a real, documented rule -(the constraint above is directly stated in VRChat's own docs), but the deeper "why" at the -level of how network IDs get allocated isn't something either UdonSharp's public +(the constraint above is directly stated in the VRChat docs), but the deeper "why" at the +level of how network IDs get allocated isn't something either the public UdonSharp documentation or its compiler source spells out in enough detail to state as fact here. Treat the rule itself as solid; treat any explanation of the underlying networking mechanics beyond it as unverified. diff --git a/docs/tsvrc/explanations/sync-model.md b/docs/core/explanations/sync-model.md similarity index 77% rename from docs/tsvrc/explanations/sync-model.md rename to docs/core/explanations/sync-model.md index ffb79e1..834a712 100644 --- a/docs/tsvrc/explanations/sync-model.md +++ b/docs/core/explanations/sync-model.md @@ -6,23 +6,23 @@ sidebar_position: 3 # Decision note: the shared-memory and sync model -Every networked TsVRC type that needs to replicate state, from +Every networked Core type that needs to replicate state, from [`TsvrcMemory`](../networking-data/tsvrc-memory) to [`Process`](../core-concepts/process) and everything built on it, to [`TsvrcTimer`](../networking-data/tsvrc-timer), uses the -same two ingredients: VRChat's manual sync mode (`BehaviourSyncMode.Manual`) plus an -explicit `RequestSerialization()` call whenever something changes, rather than VRChat's +same two ingredients: the VRChat manual sync mode (`BehaviourSyncMode.Manual`) plus an +explicit `RequestSerialization()` call whenever something changes, rather than the VRChat automatic continuous sync. This note ties together why that combination is the framework's consistent default, synthesizing what each individual type's own reference page already establishes rather than introducing new claims. ## Manual sync, sent on demand -[VRChat's own documentation of the two sync +[The VRChat documentation of the two sync modes](https://creators.vrchat.com/worlds/udon/networking/variables/#2-manual-sync) states that manual sync "requires calling `RequestSerialization()` to send data updates" and is "best for crucial values that do not change often," while continuous sync "does not require -calling `RequestSerialization()`" and instead "updates automatically" on VRChat's own -schedule, better suited to "frequently updated values." Every TsVRC type built on `Process` +calling `RequestSerialization()`" and instead "updates automatically" on the VRChat +schedule, better suited to "frequently updated values." Every Core type built on `Process` picks manual sync deliberately: state changes in these types are discrete events (a process starts, a timer pauses, a player gets added to a tracked set), not continuously-varying values like a transform position, so there's a well-defined moment to trigger a sync rather @@ -32,15 +32,15 @@ than a reason to pay for automatic periodic replication. `Process` layers a second mechanism on top of that: while a process is running, its owner re-broadcasts its full synced state on a slower, independent cadence (every 5 seconds), -regardless of whether anything actually changed since the last discrete send. `Process`'s -own source states the reason directly: the discrete, event-driven `[NetworkCallable]` -broadcasts TsVRC's own process types use for lifecycle notifications aren't -delivery-confirmed, so a client that missed one has no other way to catch up. VRChat's own +regardless of whether anything actually changed since the last discrete send. The `Process` +source states the reason directly: the discrete, event-driven `[NetworkCallable]` +broadcasts the Core process types use for lifecycle notifications aren't +delivery-confirmed, so a client that missed one has no other way to catch up. The VRChat networking docs don't promise every network event arrives; the one case they document explicitly is a client whose own rate limit is exceeded by a sender running a different, incompatible world version, where [the server "may silently drop events and not deliver them"](https://creators.vrchat.com/worlds/udon/networking/events/#mismatched-world-versions-in-the-same-instance). -TsVRC's heartbeat isn't scoped only to that one documented case: it treats every discrete +The Core heartbeat isn't scoped only to that one documented case: it treats every discrete broadcast as unconfirmed and resyncs regardless, which self-heals a missed broadcast within a few seconds without any acknowledgement or retry logic needed anywhere in the framework. See [`Process`](../core-concepts/process) for the full mechanics, including how ownership @@ -48,11 +48,11 @@ recovery interacts with this heartbeat when the previous owner leaves mid-run. ## Working around what can't be synced directly -`TsvrcMemory`'s synced tier exists specifically because `DataDictionary`, the natural -representation for an arbitrary key-value store, isn't itself a syncable type. [VRChat's own +The synced tier of `TsvrcMemory` exists specifically because `DataDictionary`, the natural +representation for an arbitrary key-value store, isn't itself a syncable type. [The VRChat list of types synced variables support](https://udonsharp.docs.vrchat.com/vrchat-api/#synced-variables) covers primitives, -Unity's small value types (`Vector2`/`3`/`4`, `Quaternion`, `Color`), strings, and `VRCUrl`; +the small Unity value types (`Vector2`/`3`/`4`, `Quaternion`, `Color`), strings, and `VRCUrl`; `DataDictionary` isn't among them. Serializing it to a plain string via [`TsJson`](../utilities/ts-json) before every synced write, and parsing it back on `OnDeserialization`, is the accommodation: everything @@ -66,10 +66,10 @@ the shared pattern behind them. Every one of these types transfers object ownership to the local player before writing a value it wants that write to propagate (`TsvrcMemory.Set` on a synced key, -`Process.SetProcessOwner`, and so on). [VRChat's own ownership +`Process.SetProcessOwner`, and so on). [The VRChat ownership documentation](https://creators.vrchat.com/worlds/udon/networking/ownership/#how-ownership-works) states plainly that "the owner of a networked object can modify its synchronized Udon variables," so only the current owner's `RequestSerialization` calls actually produce an outbound sync for anyone else. This is the same underlying VRChat ownership model in every -case; TsVRC doesn't introduce a second ownership concept of its own, it just applies -VRChat's existing one consistently everywhere state needs to move between clients. +case; Core doesn't introduce a second ownership concept of its own, it just applies +the existing VRChat one consistently everywhere state needs to move between clients. diff --git a/docs/tsvrc/first-behaviour.md b/docs/core/first-behaviour.md similarity index 75% rename from docs/tsvrc/first-behaviour.md rename to docs/core/first-behaviour.md index 79a609b..ef2d8f5 100644 --- a/docs/tsvrc/first-behaviour.md +++ b/docs/core/first-behaviour.md @@ -6,17 +6,17 @@ sidebar_position: 2 # Build your first behaviour -This walks you through initializing TsVRC in a scene and getting one script running in -play mode. By the end you'll have a world-specific behaviour that TsVRC constructs at +This walks you through initializing Core in a scene and getting one script running in +play mode. By the end you'll have a world-specific behaviour that Core constructs at startup and that logs through the framework's own logger. -This assumes TsVRC is already installed. See [Add TsVRC to your +This assumes Core is already installed. See [Add TsVRC Core to your project](./add-to-your-project) first if your project doesn't have a **Tsvrc** menu in the menu bar yet. -## Initialize TsVRC in your scene +## Initialize Core in your scene -Save your scene first. Open **Tsvrc > Configure**: on a scene that hasn't used TsVRC +Save your scene first. Open **Tsvrc > Configure**: on a scene that hasn't used Core before, the window tells you it isn't set up yet and offers a single button: **Initialize Tsvrc**. Click it. @@ -26,14 +26,14 @@ generated root script for your project and compiles it. Wait for Unity to finish recompiling before continuing: the Configure window shows a "waiting for scripts to compile" message while that's in progress. -This first run also links this scene to TsVRC, since only a saved scene has a file path -to link to. TsVRC reads its configuration from exactly that one linked scene from now on, +This first run also links this scene to Core, since only a saved scene has a file path +to link to. Core reads its configuration from exactly that one linked scene from now on, no matter what else is open. If your project uses more than one scene, see [TsLinkedScene](./codegen/config/linked-scene) before you go further. ## Write your behaviour -TsVRC generates a project-specific subclass of its internal root types so your code gets a +Core generates a project-specific subclass of its internal root types so your code gets a typed `_ts` reference back to your own project instead of a generic one. That means your script extends the generated shadow class, not the framework class it shadows. For an ordinary behaviour, that shadow class is called `TsBehaviour`: @@ -51,22 +51,22 @@ public class HelloWorld : TsBehaviour ``` `TsStart` is the framework's initialization hook, called once per instance. `LogInfo` -routes through TsVRC's own logger instead of `Debug.Log`, so the message respects whatever +routes through the Core logger instead of `Debug.Log`, so the message respects whatever log levels you've configured in **Tsvrc > Configure > Logging**. Attach `HelloWorld` to a GameObject in your scene. ## Register it as a construct -Adding the component to the scene isn't enough by itself: TsVRC only calls `TsStart` on +Adding the component to the scene isn't enough by itself: Core only calls `TsStart` on behaviours it's been told about. Back in **Tsvrc > Configure**, open the **Constructs** -tab and click **+ Add**. With `HelloWorld`'s GameObject selected, drag its `HelloWorld` +tab and click **+ Add**. With the `HelloWorld` GameObject selected, drag its `HelloWorld` component (from the Inspector, not the GameObject from the Hierarchy) into the new slot. -TsVRC needs the actual component reference here and silently rejects anything else, a +Core needs the actual component reference here and silently rejects anything else, a GameObject included, with a console warning, so dropping the GameObject by mistake looks like nothing happened. -Registering it here is what makes TsVRC call `TsStart` on it once at world startup. The +Registering it here is what makes Core call `TsStart` on it once at world startup. The reference itself stays private: if another behaviour needs to reach `HelloWorld` directly, wire it in the Inspector like any other field, or register it as a [Global](./codegen/modules/global-module) instead if you want it reachable as @@ -87,7 +87,7 @@ of leaving you to search the console for them. ## Where to go next -Read [How TsVRC fits together](./core-concepts/how-it-fits-together) for the bigger picture behind what +Read [How Core fits together](./core-concepts/how-it-fits-together) for the bigger picture behind what just happened: why there's a generation step at all, and how the pieces you touched here (the root object, the construct registration, the generated shadow class) relate to each other. diff --git a/docs/tsvrc/game-flow/ranked-game-session.md b/docs/core/game-flow/ranked-game-session.md similarity index 95% rename from docs/tsvrc/game-flow/ranked-game-session.md rename to docs/core/game-flow/ranked-game-session.md index d8fc2fe..daa2d81 100644 --- a/docs/tsvrc/game-flow/ranked-game-session.md +++ b/docs/core/game-flow/ranked-game-session.md @@ -21,7 +21,7 @@ a task-oriented walkthrough. ## Getting an instance `RankedGameSession` itself, not just the sub-behaviours it composes, ships as one of -[TsVRC's own default pool entries](../explanations/builtin-registrations) +[the Core default pool entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** `[WirePool][SerializeField] private RankedGameSession _session;` just @@ -49,10 +49,10 @@ decide what "completed" or "should end" actually means for your game. ## The state machine `CurrentState` is one of `RankedGameSessionState.Idle`, `Loading`, or `InGame`. It's a plain -local field, not re-derived from the sub-trackers' own synced state on every read: `TsStart` +local field, not re-derived from the sub-trackers own synced state on every read: `TsStart` seeds it once from that live state (safe only there, since a late-joining client's sub-trackers are already fully synced by the time its own scripts start running), and from -then on `RankedGameSession`'s own code sets it directly at each transition, whether that +then on the `RankedGameSession` code sets it directly at each transition, whether that code is running in response to a subscribed sub-behaviour event or, for `StopSession` specifically, directly inside the call itself. Re-deriving it live on every read instead would go stale on any client that doesn't own that particular sub-process. @@ -81,15 +81,15 @@ session.SetReady(); session.AddCompletedPlayer(somePlayerId); ``` -`StartLobbyTracking`/`StopLobbyTracking` are thin forwards to `_lobbyTracker`'s own -tracking. `AddLobbyPlayer`/`RemoveLobbyPlayer` add or remove one player from the lobby, and +`StartLobbyTracking`/`StopLobbyTracking` are thin forwards that start and stop tracking +on `_lobbyTracker`. `AddLobbyPlayer`/`RemoveLobbyPlayer` add or remove one player from the lobby, and both require `StartLobbyTracking` to have run first: calling either before that logs an error and does nothing, rather than silently queuing the player for later. `StartSession` snapshots the current lobby roster and starts the ready check over it; it logs an error and does nothing if a session is already running (`CurrentState` isn't `Idle`). It takes an optional `additionalKnownPresentPlayerIds`: extra IDs unioned into that -initial snapshot even if `_lobbyTracker`'s own synced state hasn't caught up to them yet, +initial snapshot even if the synced state of `_lobbyTracker` hasn't caught up to them yet, since a caller's own local knowledge (say, a trigger volume's roster) can be ahead of what's propagated over the network. Starting with an empty roster is rejected outright with an error, rather than allowed to silently hang: `ReadyCheckProcess.CheckAllPlayersReady` never @@ -155,7 +155,7 @@ through the ready check's own networking. `GamePlayerIds`/`CompletedPlayerIds` read live off the sub-trackers, which are already being torn down by the time `OnSessionEndedEvent`/`OnSessionStoppedEvent` actually fire: reading -them from inside those events' handlers sees either an already-emptied result (on whichever +them from inside those events handlers sees either an already-emptied result (on whichever client owns the sub-tracker being stopped) or a stale one that hasn't caught up yet (on every other client), never reliably the picture at the moment the round actually ended. Use `LastEndedGamePlayerIds`/`LastEndedCompletedPlayerIds` instead: both are snapshotted diff --git a/docs/tsvrc/game-flow/state-manager.md b/docs/core/game-flow/state-manager.md similarity index 94% rename from docs/tsvrc/game-flow/state-manager.md rename to docs/core/game-flow/state-manager.md index ccb54e5..df2a9b3 100644 --- a/docs/tsvrc/game-flow/state-manager.md +++ b/docs/core/game-flow/state-manager.md @@ -9,19 +9,19 @@ sidebar_position: 1 `Tsvrc.StateMachine.StateManager` is a generic, local-only (unsynced) finite state machine: states are plain integers, each with optional enter/exit method dispatch on a chosen target behaviour. Its generated shadow is `TsStateManager`. It has no dependency on any other -TsVRC module beyond `TsvrcBehaviour`. See [Give a screen or flow its own state +Core module beyond `TsvrcBehaviour`. See [Give a screen or flow its own state machine](../how-to/adding-a-screen-state-machine) for a task-oriented walkthrough. ## Getting an instance -`StateManager` ships as one of [TsVRC's own default factory +`StateManager` ships as one of [the Core default factory entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** `_ts.CreateStateManager(parent)` just works, nothing to register in Configure first. - **Manual:** skip the factory entirely. `AddComponent` it (or drag the shipped `StateManager` prefab into your scene) and start calling `RegisterState`/`SetState` - directly. Unlike most other TsVRC runtime types, `StateManager` never overrides + directly. Unlike most other Core runtime types, `StateManager` never overrides `TsStart` and never touches `_ts`, so it doesn't need `TsConstruct` to have run at all: first-class support for the fully manual path. @@ -88,6 +88,6 @@ them rather than letting an exception escape into `SetState`. A registered target whose `GameObject` was destroyed after `RegisterState` is treated the same as a `null` target: its dispatch is skipped silently rather than throwing, using the -same "compare to `null` through Unity's overridden equality" check used elsewhere in the +same "compare to `null` through the overridden Unity equality" check used elsewhere in the framework. A destroyed target for one state doesn't affect dispatch for any other registered state. diff --git a/docs/tsvrc/how-to/adding-a-language.md b/docs/core/how-to/adding-a-language.md similarity index 97% rename from docs/tsvrc/how-to/adding-a-language.md rename to docs/core/how-to/adding-a-language.md index 667c546..78ca617 100644 --- a/docs/tsvrc/how-to/adding-a-language.md +++ b/docs/core/how-to/adding-a-language.md @@ -74,7 +74,7 @@ string or an exception). ## Why this shape -See [`TranslationModule`'s reference page](../codegen/modules/translation-module) for +See the [`TranslationModule` reference page](../codegen/modules/translation-module) for exactly how a text object is discovered as a translation target, why the batch application spreads across multiple frames instead of updating everything at once, and the required `"key"`/`"label"`/`"entries"` shape a language file must have. diff --git a/docs/tsvrc/how-to/adding-a-screen-state-machine.md b/docs/core/how-to/adding-a-screen-state-machine.md similarity index 94% rename from docs/tsvrc/how-to/adding-a-screen-state-machine.md rename to docs/core/how-to/adding-a-screen-state-machine.md index 4818fef..49e8d38 100644 --- a/docs/tsvrc/how-to/adding-a-screen-state-machine.md +++ b/docs/core/how-to/adding-a-screen-state-machine.md @@ -13,7 +13,7 @@ instead of a pile of booleans. ## Steps 1. Spawn one for the screen or flow that needs it. `StateManager` ships as [one of - TsVRC's own default factory entries](../explanations/builtin-registrations), so + the Core default factory entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first. Give each screen its own instance rather than sharing one across unrelated screens: @@ -70,7 +70,7 @@ non-networked spawn. ## Why this shape -See [`StateManager`'s reference page](../game-flow/state-manager) for what happens if an +See the [`StateManager` reference page](../game-flow/state-manager) for what happens if an enter/exit method or a subscriber throws mid-transition, how a reentrant `SetState` call from inside an enter/exit method is queued rather than interleaved, and why `StateManager` needs no `TsConstruct` at all if you want to skip the factory and use one completely diff --git a/docs/tsvrc/how-to/assigning-player-colors.md b/docs/core/how-to/assigning-player-colors.md similarity index 95% rename from docs/tsvrc/how-to/assigning-player-colors.md rename to docs/core/how-to/assigning-player-colors.md index ec16e27..c5eba50 100644 --- a/docs/tsvrc/how-to/assigning-player-colors.md +++ b/docs/core/how-to/assigning-player-colors.md @@ -46,6 +46,6 @@ color belongs to which player. ## Why this shape -See [`PlayerColorAssigner`'s reference page](../players-tracking/player-color-assigner) +See the [`PlayerColorAssigner` reference page](../players-tracking/player-color-assigner) for why the palette is shuffled per instance instead of used in raw ID order, and why `GetColor` is always safe to call even before `Initialize` has run anywhere. diff --git a/docs/tsvrc/how-to/building-a-list-ui.md b/docs/core/how-to/building-a-list-ui.md similarity index 90% rename from docs/tsvrc/how-to/building-a-list-ui.md rename to docs/core/how-to/building-a-list-ui.md index fbc3fed..0dff0df 100644 --- a/docs/tsvrc/how-to/building-a-list-ui.md +++ b/docs/core/how-to/building-a-list-ui.md @@ -13,7 +13,7 @@ entry, using [`TsvrcList`](../ui-components/list/tsvrc-list) and ## Steps -1. In the scene, add a `TsvrcList` next to a `ScrollRect`. Assign the `ScrollRect`'s +1. In the scene, add a `TsvrcList` next to a `ScrollRect`. Assign the `ScrollRect` `Content` transform as its item container, a prefab carrying your `ListItem` subclass as its item prefab, and a page size (or `-1` to show every row without pagination) in the Inspector. @@ -66,7 +66,7 @@ if one's assigned in the Inspector. ## Why this shape -See [`TsvrcList`](../ui-components/list/tsvrc-list) and -[`ListItem`](../ui-components/list/list-item)'s reference pages for exactly how row +See the [`TsvrcList`](../ui-components/list/tsvrc-list) and +[`ListItem`](../ui-components/list/list-item) reference pages for exactly how row instances get reused across pages instead of destroyed and recreated, and what -`Bind`/`Unbind` guarantee about `_itemData`'s lifetime inside `_OnUnbind`. +`Bind`/`Unbind` guarantee about the lifetime of `_itemData` inside `_OnUnbind`. diff --git a/docs/tsvrc/how-to/combining-meshes.md b/docs/core/how-to/combining-meshes.md similarity index 88% rename from docs/tsvrc/how-to/combining-meshes.md rename to docs/core/how-to/combining-meshes.md index 5792c4f..e65ff1b 100644 --- a/docs/tsvrc/how-to/combining-meshes.md +++ b/docs/core/how-to/combining-meshes.md @@ -29,7 +29,7 @@ keep working). ## Deciding whether to recalculate normals Leave **Recalculate Normals** off (the default) to preserve baked or hand-authored -normals from the source meshes. Turn it on only if the sources' own normals don't look +normals from the source meshes. Turn it on only if the sources own normals don't look right once merged, since recalculating from the combined geometry can smooth over intentional hard edges. @@ -37,13 +37,13 @@ intentional hard edges. Undo reverses the new GameObject, the recreated collider children, and the source deactivation as one step. It doesn't remove the saved mesh asset file (and the -`_Collider` asset alongside it, if one was created): Unity's own Undo system doesn't +`_Collider` asset alongside it, if one was created): the Unity Undo system doesn't track saving an asset to disk. Delete those files by hand if you undo a combine you don't want to keep. ## Why this shape -See [the Mesh Combiner's reference page](../utilities/mesh-combiner) for exactly which +See [the Mesh Combiner reference page](../utilities/mesh-combiner) for exactly which colliders merge versus get recreated as children, how a source carrying an `UdonSharpBehaviour` is handled differently throughout, and the two conditions that disable the Combine button outright rather than letting it produce a broken result. diff --git a/docs/tsvrc/how-to/guarding-against-head-clipping.md b/docs/core/how-to/guarding-against-head-clipping.md similarity index 90% rename from docs/tsvrc/how-to/guarding-against-head-clipping.md rename to docs/core/how-to/guarding-against-head-clipping.md index 06a4f9f..8b01e1c 100644 --- a/docs/tsvrc/how-to/guarding-against-head-clipping.md +++ b/docs/core/how-to/guarding-against-head-clipping.md @@ -12,8 +12,8 @@ comfort setting rather than something every world forces on. ## Steps -1. Spawn one with the generated factory method. `HeadClipGuard` ships as [one of TsVRC's - own default factory entries](../explanations/builtin-registrations), so there's +1. Spawn one with the generated factory method. `HeadClipGuard` ships as [one of the Core + default factory entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first: ```csharp @@ -48,7 +48,7 @@ transform once and the guard re-evaluates all of them every frame it's active. ## Why this shape -See [`HeadClipGuard`'s reference page](../players-tracking/head-clip-guard) for how it +See the [`HeadClipGuard` reference page](../players-tracking/head-clip-guard) for how it decides which direction to push, the three-tier movement gate/batch-scan/full-test pipeline that keeps per-frame cost down with many colliders, and the two tunable invariants (`_margin`, `_batchScanExpansion`) worth understanding before changing them. diff --git a/docs/tsvrc/how-to/persisting-a-value.md b/docs/core/how-to/persisting-a-value.md similarity index 91% rename from docs/tsvrc/how-to/persisting-a-value.md rename to docs/core/how-to/persisting-a-value.md index da625fd..c323f50 100644 --- a/docs/tsvrc/how-to/persisting-a-value.md +++ b/docs/core/how-to/persisting-a-value.md @@ -7,7 +7,7 @@ sidebar_position: 6 # Persist a value across visits How to save a value for the local player that's still there the next time they load your -world, using [`TsvrcMemory`](../networking-data/tsvrc-memory)'s persistent tier. No +world, using the persistent tier of [`TsvrcMemory`](../networking-data/tsvrc-memory). No registration or pooling is needed to reach it at all: it's always available as `_ts.Memory`. ## Steps @@ -89,7 +89,7 @@ piling every key into one root class as the world grows. ## Why this shape -See [`TsvrcMemory`'s reference page](../networking-data/tsvrc-memory) for the full +See the [`TsvrcMemory` reference page](../networking-data/tsvrc-memory) for the full contract: the three isolated tiers (ephemeral/persistent/synced), why `Add` and `Set` -behave differently, VRChat's own PlayerData size and timing limits, and what happens if +behave differently, the VRChat PlayerData size and timing limits, and what happens if you register the same key twice or write to it before it's restored. diff --git a/docs/tsvrc/how-to/pooling-a-prefab.md b/docs/core/how-to/pooling-a-prefab.md similarity index 91% rename from docs/tsvrc/how-to/pooling-a-prefab.md rename to docs/core/how-to/pooling-a-prefab.md index 2f55b36..b228919 100644 --- a/docs/tsvrc/how-to/pooling-a-prefab.md +++ b/docs/core/how-to/pooling-a-prefab.md @@ -35,7 +35,7 @@ here at all. ``` This is a code change, not a Configure window edit, so it needs no Apply click at all: - once Unity finishes compiling, TsVRC's generator runs automatically and discovers the + once Unity finishes compiling, the Core generator runs automatically and discovers the field by reflection. `PoolModule` computes how many total slots the project needs across every `[WirePool]` field referencing this type, generates one serialized field per slot, and instantiates and wires them all under a `"Pool"` container at wire time. @@ -70,12 +70,12 @@ never registered on the Pools tab logs a warning and leaves the field `null` for rather than failing generation outright. Registering the type and regenerating resolves it. -If the field's type is one of TsVRC's own runtime types (`PlayerTracker`, `TsvrcTimer`, +If the field's type is one of the Core runtime types (`PlayerTracker`, `TsvrcTimer`, and four others), skip step 1 entirely: it's already registered for you. See [Decision note: builtin pool and factory entries](../explanations/builtin-registrations). ## Why this shape See [the pooling pattern](../explanations/pooling-pattern) for the underlying VRChat -networking constraint this works around, and [`PoolModule`'s reference page](../codegen/modules/pool-module) +networking constraint this works around, and the [`PoolModule` reference page](../codegen/modules/pool-module) for the exact slot-count formula and what happens on a broken compile mid-generation. diff --git a/docs/tsvrc/how-to/reaching-a-scene-object.md b/docs/core/how-to/reaching-a-scene-object.md similarity index 92% rename from docs/tsvrc/how-to/reaching-a-scene-object.md rename to docs/core/how-to/reaching-a-scene-object.md index 75ce2e6..c5a802f 100644 --- a/docs/tsvrc/how-to/reaching-a-scene-object.md +++ b/docs/core/how-to/reaching-a-scene-object.md @@ -42,8 +42,8 @@ project. Register it as both if you need both: a Global entry that happens to be } ``` -Any scene object works as a Global, not just a `TsvrcBehaviour`, since a Global's whole -point is naming, not initialization. A plain `GameObject` or an arbitrary `Component` +Any scene object works as a Global, not just a `TsvrcBehaviour`, since the whole point +of a Global is naming, not initialization. A plain `GameObject` or an arbitrary `Component` reference is exposed the same way, just never constructed. ## Steps: registering a Construct @@ -73,7 +73,7 @@ arbitrary `GameObject` reference doesn't qualify. ## Why this shape -See [`GlobalModule`](../codegen/modules/global-module) and -[`ConstructModule`](../codegen/modules/construct-module)'s reference pages for how each +See the [`GlobalModule`](../codegen/modules/global-module) and +[`ConstructModule`](../codegen/modules/construct-module) reference pages for how each derives a default name, what happens on a name collision, and how tree-shaking can exclude an unused Global if your project has `TreeShakeUnused` turned on. diff --git a/docs/tsvrc/how-to/running-a-countdown.md b/docs/core/how-to/running-a-countdown.md similarity index 92% rename from docs/tsvrc/how-to/running-a-countdown.md rename to docs/core/how-to/running-a-countdown.md index 2fd12d1..e6cfd67 100644 --- a/docs/tsvrc/how-to/running-a-countdown.md +++ b/docs/core/how-to/running-a-countdown.md @@ -11,7 +11,7 @@ How to run a countdown every client sees identically, using ## Steps -1. Declare a `[WirePool]` field. `TsvrcTimer` ships as [one of TsVRC's own default pool +1. Declare a `[WirePool]` field. `TsvrcTimer` ships as [one of the Core default pool entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first: @@ -73,7 +73,7 @@ protected override void TsStart() ## Why this shape -See [`TsvrcTimer`'s reference page](../networking-data/tsvrc-timer) for the full event +See the [`TsvrcTimer` reference page](../networking-data/tsvrc-timer) for the full event table, how pausing interacts with the timer's own auto-complete check, and the two sharp edges around detecting a late joiner or a stop-then-restart landing in the same network packet. diff --git a/docs/tsvrc/how-to/running-a-timed-round.md b/docs/core/how-to/running-a-timed-round.md similarity index 97% rename from docs/tsvrc/how-to/running-a-timed-round.md rename to docs/core/how-to/running-a-timed-round.md index b5c78b7..ee071e4 100644 --- a/docs/tsvrc/how-to/running-a-timed-round.md +++ b/docs/core/how-to/running-a-timed-round.md @@ -13,7 +13,7 @@ ready check, and timer yourself. ## Steps -1. Declare a `[WirePool]` field. `RankedGameSession` ships as [one of TsVRC's own default +1. Declare a `[WirePool]` field. `RankedGameSession` ships as [one of the Core default pool entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first: @@ -102,7 +102,7 @@ public bool IsPlayerStillPlaying(string playerId) => ## Why this shape -See [`RankedGameSession`'s reference page](../game-flow/ranked-game-session) for the full +See the [`RankedGameSession` reference page](../game-flow/ranked-game-session) for the full state machine, the nine lifecycle events, and a real asymmetry worth knowing before you build a networked "force stop" button: `StopSession` reaches every client automatically during the lobby's ready check, but not once a round is already in progress. diff --git a/docs/core/how-to/set-up-automated-testing.md b/docs/core/how-to/set-up-automated-testing.md new file mode 100644 index 0000000..bc9b894 --- /dev/null +++ b/docs/core/how-to/set-up-automated-testing.md @@ -0,0 +1,39 @@ +--- +id: set-up-automated-testing +title: Set up automated testing +sidebar_position: 5 +--- + +# Set up automated testing + +Set up your project for Play Mode and Edit Mode tests against your own `UdonSharpBehaviour`s, +written with [Udon Test Kit](/docs/udon-test-kit/intro). + +## 1. Put your scripts and generated code in one assembly + +The code Core generates declares the base types your world scripts extend (`TsBehaviour`, +`TsInstance`, and so on), and in turn references concrete types from your own scripts: a +Construct or Factory entry generates a field typed as whatever class you registered. That's +a two-way dependency, and Unity doesn't allow two assembly definitions to reference each +other. Give your project's own scripts folder a single `asmdef` placed high enough in the +folder tree to cover both your hand-written scripts and wherever the generated-folder +setting in your `TsConfig` points (they don't need to be nested inside each other, just both +under that one asmdef's root), referencing `Tsvrc.Runtime` plus whatever VRChat SDK/ +UdonSharp assemblies your scripts use. + +## 2. Create your test assemblies + +Core depends on Udon Test Kit, so the kit is already in your project. Create your test +assemblies with its menu item, as described in +[Create your test assemblies](/docs/udon-test-kit/add-to-your-project#create-your-test-assemblies). +They already reference the kit, the VRChat SDK, ClientSim and your project's assembly from +step 1. + +## 3. Reference Tsvrc.Runtime + +Add `Tsvrc.Runtime` to the references of the `EditMode` and `PlayMode` test assemblies if it isn't +listed already, and to the `Doubles` assembly if your test doubles extend Core types. + +That's all. The Core codegen holds back on its own while tests run, so nothing else needs +arming. [Testing your world](../testing/testing-your-world) covers how a test builds your +behaviours. diff --git a/docs/tsvrc/how-to/showing-player-positions.md b/docs/core/how-to/showing-player-positions.md similarity index 100% rename from docs/tsvrc/how-to/showing-player-positions.md rename to docs/core/how-to/showing-player-positions.md diff --git a/docs/tsvrc/how-to/spawning-a-local-object.md b/docs/core/how-to/spawning-a-local-object.md similarity index 96% rename from docs/tsvrc/how-to/spawning-a-local-object.md rename to docs/core/how-to/spawning-a-local-object.md index 98e54ad..c1173eb 100644 --- a/docs/tsvrc/how-to/spawning-a-local-object.md +++ b/docs/core/how-to/spawning-a-local-object.md @@ -50,7 +50,7 @@ the same prefab. ## Why this shape -See [`FactoryModule`'s reference page](../codegen/modules/factory-module) for how the +See the [`FactoryModule` reference page](../codegen/modules/factory-module) for how the instantiation source is a pre-instantiated, inactive template rather than the raw prefab asset, and why that specific detail is what makes factory-created objects fast to spawn repeatedly. See [Decision note: builtin pool and factory entries](../explanations/builtin-registrations) diff --git a/docs/tsvrc/how-to/tracking-a-group-of-players.md b/docs/core/how-to/tracking-a-group-of-players.md similarity index 95% rename from docs/tsvrc/how-to/tracking-a-group-of-players.md rename to docs/core/how-to/tracking-a-group-of-players.md index d084d03..307e73d 100644 --- a/docs/tsvrc/how-to/tracking-a-group-of-players.md +++ b/docs/core/how-to/tracking-a-group-of-players.md @@ -20,7 +20,7 @@ everyone and can't be told to track less. ## Steps -1. Declare a `[WirePool]` field. `PlayerTracker` ships as [one of TsVRC's own default pool +1. Declare a `[WirePool]` field. `PlayerTracker` ships as [one of the Core default pool entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first: @@ -64,7 +64,7 @@ everyone and can't be told to track less. ## Why this shape -See [`PlayerTracker`'s reference page](../players-tracking/player-tracker) for the full +See the [`PlayerTracker` reference page](../players-tracking/player-tracker) for the full event table, exactly when a `Notify*` broadcast fires versus when `LastPlayerIds` itself updates, and the three separate paths that remove a player automatically (leaving, suspending, or the tracker's own owner abandoning it). diff --git a/docs/tsvrc/how-to/transferring-data-between-clients.md b/docs/core/how-to/transferring-data-between-clients.md similarity index 99% rename from docs/tsvrc/how-to/transferring-data-between-clients.md rename to docs/core/how-to/transferring-data-between-clients.md index 08ee985..6a4d2c4 100644 --- a/docs/tsvrc/how-to/transferring-data-between-clients.md +++ b/docs/core/how-to/transferring-data-between-clients.md @@ -13,7 +13,7 @@ understand what's happening underneath this API. ## Steps -1. Declare a `[WirePool]` field for it. `DataTransferer` ships as [one of TsVRC's own +1. Declare a `[WirePool]` field for it. `DataTransferer` ships as [one of the Core default pool entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first. 2. Subscribe to completion before you ever call `TransferData`, so you don't miss a transfer diff --git a/docs/tsvrc/how-to/wiring-a-ready-check.md b/docs/core/how-to/wiring-a-ready-check.md similarity index 95% rename from docs/tsvrc/how-to/wiring-a-ready-check.md rename to docs/core/how-to/wiring-a-ready-check.md index f382a1b..d4a32d9 100644 --- a/docs/tsvrc/how-to/wiring-a-ready-check.md +++ b/docs/core/how-to/wiring-a-ready-check.md @@ -12,7 +12,7 @@ How to block an action until every present player has confirmed they're ready, u ## Steps -1. Declare a `[WirePool]` field for each. Both ship as [TsVRC's own default pool +1. Declare a `[WirePool]` field for each. Both ship as [the Core default pool entries](../explanations/builtin-registrations), so there's nothing to register on the Configure window first, and codegen calls `TsConstruct` for you at wire time: @@ -90,4 +90,4 @@ field of the same type. [its reference page](../players-tracking/ready-check-process) for the ordering guarantees around `SetReady` and what happens when the tracked set changes mid-check. For the "why generated code, why not just call these directly at compile time" background, see -[How TsVRC fits together](../core-concepts/how-it-fits-together). +[How Core fits together](../core-concepts/how-it-fits-together). diff --git a/docs/core/how-to/writing-your-first-test.md b/docs/core/how-to/writing-your-first-test.md new file mode 100644 index 0000000..291a32b --- /dev/null +++ b/docs/core/how-to/writing-your-first-test.md @@ -0,0 +1,90 @@ +--- +id: writing-your-first-test +title: Write your first automated test for your own behaviour +sidebar_position: 18 +--- + +# Write your first automated test for your own behaviour + +How to write a real Play Mode test against your own `TsvrcBehaviour` subclass with +[Udon Test Kit](/docs/udon-test-kit/intro), once [your test assemblies are set +up](./set-up-automated-testing). + +## Steps + +1. Extend the kit's [`ClientSimTestBase`](/docs/udon-test-kit/reference/clientsim-test-base), give + the class a scene of its own, and start ClientSim in a `[UnityTest]`: + + ```csharp + public class RoundRefereeTests : ClientSimTestBase + { + public RoundRefereeTests() : base(TestScene.Empty("RoundRefereeTests")) + { + } + + [UnityTest] + public IEnumerator BeginRound_Owner_StartsTheRound() + { + yield return Session.Start(); + ``` + + Every test gets a fresh empty scene, and everything it creates goes when the scene unloads. + +2. Add the behaviour under test to a GameObject and construct it, as your generated root does at + world startup: + + ```csharp + var referee = new GameObject("RoundReferee").AddComponent(); + referee.TsConstruct((TsRoot)null); + ``` + + Pass `null` when the behaviour doesn't reach `_ts` during the test. + +3. Drive the behaviour and assert on it: + + ```csharp + referee.BeginRound(); + + Assert.IsTrue(referee.IsProcessRunning()); + } + } + ``` + +## Reaching a private field or method + +Use [`PrivateFieldAccess`](/docs/udon-test-kit/reference/private-field-access) from +Udon Test Kit instead of making something `public` just so a test can see it: + +```csharp +int remaining = PrivateFieldAccess.GetField(referee, "_remainingMilliseconds"); +``` + +## Asserting an event actually fired + +Subscribe a spy from the Udon Test Kit +[`CallLog`](/docs/udon-test-kit/reference/callback-spy-and-call-log) instead of writing a +one-off listener class for every test: + +```csharp +using var calls = new CallLog(); +CallbackSpy listener = calls.CreateSpy("Listener"); +referee.TsSubscribe(listener, RoundReferee.OnRoundEndedEvent, nameof(CallbackSpy.CallbackA)); + +// ...drive the round to completion... + +Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); +``` + +The log records each call in order, so the same assertion also fails if the event fired +twice. + +## Testing as a player who isn't the owner + +A `Process` such as `RoundReferee` acts only for its owner and forwards everything else. Hand it to +a remote player with the kit's `Session.SetOwner` to test the other side, as in +[Test what happens for a player who isn't the owner](/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner). + +## Why this shape + +See [Testing your world](../testing/testing-your-world) for how a test builds your behaviours +and what the Core codegen does during a test run. diff --git a/docs/core/intro.md b/docs/core/intro.md new file mode 100644 index 0000000..82f8142 --- /dev/null +++ b/docs/core/intro.md @@ -0,0 +1,44 @@ +--- +id: intro +title: TsVRC Core +sidebar_position: 1 +--- + +# TsVRC Core + +TsVRC Core is an open-source framework for building VRChat worlds with +UdonSharp: structured initialization, dependency wiring, and an editor tool that generates +the code for both automatically (codegen), on top of the VRChat Worlds SDK. + +:::warning[Experimental and pre-1.0] +Core is an experiment, and AI coding tools generated part of its features. It isn't fully vibe +coded, but AI had a large share in writing it. It's also still pre-1.0: everything documented +here, including edge-case behavior on reference pages, describes what the code does today, not a +locked API contract. + +Test the behavior your world relies on before you publish it. For projects that are ready to use, +see [Udon Test Kit](/docs/udon-test-kit/intro) and +[Udon Tools Kit](https://github.com/tsvrc/udon-tools-kit). +::: + +## New to Core? + +Start with [Add TsVRC Core to your project](./add-to-your-project), then +[Build your first behaviour](./first-behaviour): by the end you'll have a script running +in play mode. + +## Already using it? + +Jump straight to what you need: + +- **Doing a specific task** — [pooling a prefab](./how-to/pooling-a-prefab), + [showing player positions](./how-to/showing-player-positions), + [transferring data between clients](./how-to/transferring-data-between-clients), or + [wiring a ready check](./how-to/wiring-a-ready-check). +- **Looking up a type's behavior** — search for its name, start from + [How Core fits together](./core-concepts/how-it-fits-together) and follow its links, or + browse Reference in the sidebar, grouped by area: players, networking, UI, codegen, and + so on. +- **Understanding why Core works the way it does** — + [Codegen instead of reflection](./explanations/codegen-vs-reflection) and the other pages + under Explanations. diff --git a/docs/tsvrc/networking-data/data-transfer/chunked-transfer-session.md b/docs/core/networking-data/data-transfer/chunked-transfer-session.md similarity index 93% rename from docs/tsvrc/networking-data/data-transfer/chunked-transfer-session.md rename to docs/core/networking-data/data-transfer/chunked-transfer-session.md index b691530..4955db6 100644 --- a/docs/tsvrc/networking-data/data-transfer/chunked-transfer-session.md +++ b/docs/core/networking-data/data-transfer/chunked-transfer-session.md @@ -27,8 +27,8 @@ every chunk in sequence: this is the actual "send this chunk" signal a subclass networking). If every tracked player leaves or gets suspended while a chunk's ready check is active, the -check itself can't auto-complete: [`ReadyCheckProcess`](../../players-tracking/ready-check-process)'s -own completion poll returns early on an empty tracked list rather than treating "nobody +check itself can't auto-complete: the completion poll in +[`ReadyCheckProcess`](../../players-tracking/ready-check-process) returns early on an empty tracked list rather than treating "nobody left to wait on" as "everyone's ready." `ChunkedTransferSession` overrides `OnProcessUpdate` to detect exactly that (an active process with zero tracked players) and calls `CancelDataTransfer()` itself, so a transfer with no remaining targets doesn't stall @@ -36,8 +36,8 @@ forever waiting for a ready check that can never complete on its own. ## The inter-chunk gap -Between chunks, the underlying `Process` is briefly not running: a `ReadyCheckProcess`'s -"stopped" state doesn't distinguish "the whole transfer is done" from "this chunk's check +Between chunks, the underlying `Process` is briefly not running: the "stopped" state of a +`ReadyCheckProcess` doesn't distinguish "the whole transfer is done" from "this chunk's check finished, the next one hasn't started yet." `ChunkedTransferSession` tracks that window itself with an internal, synced `_pendingNextChunk` flag, deferring the actual start of the next ready check to the following frame (`SendCustomEventDelayedSeconds(...,0f)`) so the @@ -53,7 +53,7 @@ false during the gap. Transfer state (`_dataChunks`, chunk indices) is deliberately **not** synced: only the owner needs it, and keeping it off the wire means the remaining message text is never -re-sent as a side effect of `Process`'s own [periodic +re-sent as a side effect of the `Process` [periodic auto-resync](../../core-concepts/process#the-tick-loop-and-auto-resync), which re-broadcasts every synced field every 5 seconds while a process runs. That means a new owner, promoted after the old one leaves, starts with empty chunk state no matter where in the transfer the diff --git a/docs/tsvrc/networking-data/data-transfer/data-chunk-receiver.md b/docs/core/networking-data/data-transfer/data-chunk-receiver.md similarity index 90% rename from docs/tsvrc/networking-data/data-transfer/data-chunk-receiver.md rename to docs/core/networking-data/data-transfer/data-chunk-receiver.md index 9228e77..2dd3bd5 100644 --- a/docs/tsvrc/networking-data/data-transfer/data-chunk-receiver.md +++ b/docs/core/networking-data/data-transfer/data-chunk-receiver.md @@ -7,8 +7,8 @@ sidebar_position: 4 # DataChunkReceiver `Tsvrc.DataTransfer.DataChunkReceiver` is where an incoming chunk actually gets validated, -stored, and acknowledged. It overrides [DataSender](./data-sender)'s -`OnDataChunkSendRequested` to broadcast each chunk via its own `[NetworkCallable]` method, +stored, and acknowledged. It overrides +`OnDataChunkSendRequested` from [DataSender](./data-sender) to broadcast each chunk via its own `[NetworkCallable]` method, `BroadcastDataChunkReceived`, which every instance player receives and which validates the chunk before storing it. You never call it directly. See [DataTransferer](./data-transferer) for the type a consumer actually uses. @@ -47,7 +47,7 @@ flow, not just handling malformed input: - **`totalChunks` is locked to the first chunk's value** for the rest of the transfer; a later chunk claiming a different total is rejected outright, since accepting it would silently reallocate the receive buffer and discard everything already assembled. -- **A chunk index already filled is ignored** — VRChat's own documentation [describes its +- **A chunk index already filled is ignored** — the VRChat documentation [describes its internal splitting of large events as "almost transparent"](https://creators.vrchat.com/worlds/udon/networking/events/#parameter-size-limits-and-event-splitting), which leaves room for a duplicate delivery under some conditions (network anomalies, an @@ -62,11 +62,11 @@ independent reason of its own: VRChat [splits any event payload over 1 KB into i packets](https://creators.vrchat.com/worlds/udon/networking/events/#parameter-size-limits-and-event-splitting), and each of those counts separately against the rate limit. A 2,500-character ASCII chunk is already close to 2,500 bytes (roughly three internal packets); a CJK chunk near the -worst case discussed on [`DataChunker`](./data-chunker)'s page is closer to 7,500 bytes -(roughly eight). At the SDK's default of 5 events per second, even the ASCII case would take +worst case discussed on the [`DataChunker`](./data-chunker) page is closer to 7,500 bytes +(roughly eight). At the SDK default of 5 events per second, even the ASCII case would take over half a second to clear the rate limiter per chunk, and the CJK case over a second. A 100/second budget clears either in well under a tenth of a second, comfortably inside -VRChat's [global throughput cap](https://creators.vrchat.com/worlds/udon/networking/network-details/#bandwidth-limits) +the VRChat [global throughput cap](https://creators.vrchat.com/worlds/udon/networking/network-details/#bandwidth-limits) of about 11 KB/s, which ends up being the real limit on how fast a transfer can go. ## Storing and acknowledging diff --git a/docs/tsvrc/networking-data/data-transfer/data-chunker.md b/docs/core/networking-data/data-transfer/data-chunker.md similarity index 100% rename from docs/tsvrc/networking-data/data-transfer/data-chunker.md rename to docs/core/networking-data/data-transfer/data-chunker.md diff --git a/docs/tsvrc/networking-data/data-transfer/data-sender-receiver.md b/docs/core/networking-data/data-transfer/data-sender-receiver.md similarity index 96% rename from docs/tsvrc/networking-data/data-transfer/data-sender-receiver.md rename to docs/core/networking-data/data-transfer/data-sender-receiver.md index a3bf14a..1c94181 100644 --- a/docs/tsvrc/networking-data/data-transfer/data-sender-receiver.md +++ b/docs/core/networking-data/data-transfer/data-sender-receiver.md @@ -45,7 +45,7 @@ repeated here. Most consumers use [`DataTransferer`](./data-transferer) directly and never touch this layer. Subclass `DataSenderReceiver` itself only if you want the sender/receiver plumbing without -`DataTransferer`'s transfer-specific event names. Override `OnDataReceptionCompleted` and +the transfer-specific event names of `DataTransferer`. Override `OnDataReceptionCompleted` and read `LastData`: ```csharp diff --git a/docs/tsvrc/networking-data/data-transfer/data-sender.md b/docs/core/networking-data/data-transfer/data-sender.md similarity index 95% rename from docs/tsvrc/networking-data/data-transfer/data-sender.md rename to docs/core/networking-data/data-transfer/data-sender.md index 2ca8133..fe6b027 100644 --- a/docs/tsvrc/networking-data/data-transfer/data-sender.md +++ b/docs/core/networking-data/data-transfer/data-sender.md @@ -6,8 +6,8 @@ sidebar_position: 3 # DataSender -`Tsvrc.DataTransfer.DataSender` puts real networking under -[ChunkedTransferSession](./chunked-transfer-session)'s sequencing hooks: it translates +`Tsvrc.DataTransfer.DataSender` puts real networking under the sequencing hooks of +[ChunkedTransferSession](./chunked-transfer-session): it translates `OnChunkSequenceStarted`/`Stopped`/`Completed` into `[NetworkCallable]` broadcasts to every player in the instance, and exposes them as three events (`OnDataTransferStartedEvent`, `...StoppedEvent`, `...CompletedEvent`) plus matching `protected virtual` hooks diff --git a/docs/tsvrc/networking-data/data-transfer/data-transferer.md b/docs/core/networking-data/data-transfer/data-transferer.md similarity index 90% rename from docs/tsvrc/networking-data/data-transfer/data-transferer.md rename to docs/core/networking-data/data-transfer/data-transferer.md index e589f34..944f5c6 100644 --- a/docs/tsvrc/networking-data/data-transfer/data-transferer.md +++ b/docs/core/networking-data/data-transfer/data-transferer.md @@ -15,12 +15,12 @@ for a task-oriented walkthrough. ## What it adds -Four public event constants that simply forward `DataSenderReceiver`'s reception-focused +Four public event constants that simply forward the reception-focused `DataSenderReceiver` events under transfer-focused names (`OnTransferStartedEvent`, `OnTransferStoppedEvent`, `OnTransferCompletedEvent`, `OnTransferChunkEvent`), plus one small piece of defensive history: `DataTransferer` still overrides `OnOwnerAbandonedProcess`, but the override's body -is now just a call to `base`. The base chain (`ChunkedTransferSession`'s -`OnOwnerAbandonedProcess`, inherited unchanged through `DataSender`, `DataChunkReceiver`, and +is now just a call to `base`. The base chain +(`ChunkedTransferSession.OnOwnerAbandonedProcess`, inherited unchanged through `DataSender`, `DataChunkReceiver`, and `DataSenderReceiver`) already calls `StopReadyCheck()` and fully cleans up transfer state during a handover. The override exists to carry a comment warning against re-adding an earlier version's extra `CancelDataTransfer()` call here, which would run against an @@ -28,7 +28,7 @@ already-stopped process and log a spurious warning. ## Getting an instance -`DataTransferer` ships as one of [TsVRC's own default pool +`DataTransferer` ships as one of [the Core default pool entries](../../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** a `[WirePool]` field of this type just resolves, nothing to register in diff --git a/docs/tsvrc/networking-data/overview.md b/docs/core/networking-data/overview.md similarity index 95% rename from docs/tsvrc/networking-data/overview.md rename to docs/core/networking-data/overview.md index 2537651..767e53b 100644 --- a/docs/tsvrc/networking-data/overview.md +++ b/docs/core/networking-data/overview.md @@ -11,14 +11,14 @@ inheritance chain built on [Process](../core-concepts/process): `Process → PlayerTracker → ReadyCheckProcess → DataChunker → ChunkedTransferSession → DataSender → DataChunkReceiver → DataSenderReceiver → DataTransferer`. Each layer adds one concern on top of the one below it, so this section documents them bottom-up in that same -order. Reading `DataTransferer`'s page without the ones before it skips the reasoning +order. Reading the `DataTransferer` page without the ones before it skips the reasoning behind most of what it does. ## The problem being solved VRChat caps the [total parameter size of a single network event](https://creators.vrchat.com/worlds/udon/networking/events/#parameter-size-limits-and-event-splitting) -at 16 KB and [Udon's overall outgoing +at 16 KB and [the overall outgoing Udon bandwidth](https://creators.vrchat.com/worlds/udon/networking/network-details/#bandwidth-limits) to about 11 KB/s, so sending an arbitrary string to a set of players means splitting it into chunks, sending them one at a time, and confirming each one actually arrived before diff --git a/docs/tsvrc/networking-data/tsvrc-memory.md b/docs/core/networking-data/tsvrc-memory.md similarity index 93% rename from docs/tsvrc/networking-data/tsvrc-memory.md rename to docs/core/networking-data/tsvrc-memory.md index a5ee3c1..5dc806a 100644 --- a/docs/tsvrc/networking-data/tsvrc-memory.md +++ b/docs/core/networking-data/tsvrc-memory.md @@ -6,7 +6,7 @@ sidebar_position: 1 # TsvrcMemory -`Tsvrc.Utils.TsvrcMemory` is TsVRC's shared key-value store, reached through `_ts.Memory`. +`Tsvrc.Utils.TsvrcMemory` is the shared key-value store in Core, reached through `_ts.Memory`. Its generated shadow class is `TsMemory`. It holds every key in exactly one of three isolated tiers, ephemeral, persistent, or synced, and the tier is a per-key decision you make once, before you ever write a value. See [Persist a value across @@ -16,7 +16,7 @@ visits](../how-to/persisting-a-value) for a task-oriented walkthrough. - **Ephemeral** (the default). Local to this client only, and gone when the player leaves the world. Any key you never call `Register` on lives here. -- **Persistent.** Backed by VRChat's `PlayerData`, so it survives across sessions for the +- **Persistent.** Backed by VRChat `PlayerData`, so it survives across sessions for the local player. Declared with `Register(key, persist: true, synced: false)`. - **Synced.** Replicated to every player over the network. Declared with `Register(key, persist: false, synced: true)`. @@ -61,7 +61,7 @@ TsvrcMemory.OnSyncedChangedEvent, nameof(YourCallback))`. That event fires only network-driven deserialization, never for your own local `Set` calls. Writing to a synced key you don't currently own transfers ownership to the local player -first (`Networking.SetOwner`), matching VRChat's networking model where only the owner of an +first (`Networking.SetOwner`), matching the VRChat networking model where only the owner of an object can push synced-variable updates for it. If `_syncedJson` ever fails to parse as a JSON object, whether from malformed data or a JSON @@ -71,8 +71,8 @@ them. ## PlayerData constraints (persistent tier) -These come from VRChat's own [persistence system](https://creators.vrchat.com/worlds/udon/persistence/player-data), -not from TsVRC: +These come from the VRChat [persistence system](https://creators.vrchat.com/worlds/udon/persistence/player-data), +not from Core: - VRChat allows [100 KB of PlayerData per player per world](https://creators.vrchat.com/worlds/udon/persistence/#limitations). VRChat @@ -85,7 +85,7 @@ not from TsVRC: - [Persistent data is scoped to this world](https://creators.vrchat.com/worlds/udon/persistence/#limitations); it isn't shared with any other world. -- A persistent key can't actually be deleted from PlayerData: [VRChat's own API reference +- A persistent key can't actually be deleted from PlayerData: [the VRChat API reference says the same](https://creators.vrchat.com/worlds/udon/persistence/player-data/#mutators) ("Keys cannot be deleted after being written."). `Remove` only clears the local in-memory cache, and logs a warning saying so. diff --git a/docs/tsvrc/networking-data/tsvrc-timer.md b/docs/core/networking-data/tsvrc-timer.md similarity index 93% rename from docs/tsvrc/networking-data/tsvrc-timer.md rename to docs/core/networking-data/tsvrc-timer.md index 3c79b8a..e685be6 100644 --- a/docs/tsvrc/networking-data/tsvrc-timer.md +++ b/docs/core/networking-data/tsvrc-timer.md @@ -10,13 +10,13 @@ sidebar_position: 2 [Process](../core-concepts/process). Its generated shadow is `TsTimer`. All clients share [server time](https://udonsharp.docs.vrchat.com/vrchat-api/#methods-4) anchors and compute elapsed time locally, so a running timer needs no discrete network message per tick, -beyond `Process`'s own periodic [auto-resync +beyond the periodic `Process` [auto-resync heartbeat](../core-concepts/process#the-tick-loop-and-auto-resync). See [Run a synced countdown](../how-to/running-a-countdown) for a task-oriented walkthrough. ## Getting an instance -`TsvrcTimer` ships as one of [TsVRC's own default pool +`TsvrcTimer` ships as one of [the Core default pool entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** a `[WirePool]` field of this type just resolves, nothing to register in @@ -37,7 +37,7 @@ entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). ``` - **Manual:** skip pooling entirely. Drag the shipped `TsTimer` prefab into your scene and reference it directly, but then call `TsConstruct` on it yourself before calling - anything else on it. Skipping that leaves [`Process`'s cached local-player + anything else on it. Skipping that leaves the [cached `Process` local-player ID](../core-concepts/process) at its default, which makes `IsProcessOwner()` report `false` even for the actual owner, so `StartTimer`/`StopTimer`/`PauseTimer` all behave as if called by a non-owner. @@ -57,7 +57,7 @@ changes, not for driving a per-frame UI redraw. to the owner over the network if called by a non-owner, [same as `Process`](../core-concepts/process#lifecycle). - **`PauseTimer()`** / **`ResumeTimer()`** — freeze and unfreeze elapsed time. Pausing - extends the effective run time: `OnProcessUpdate`'s auto-complete check uses + extends the effective run time: the auto-complete check in `OnProcessUpdate` uses `GetElapsedMilliseconds()`, which returns the frozen offset while paused, so a duration is always fully observed regardless of how long a pause lasts. Both forward to the owner when called by a non-owner, through their own [`[NetworkCallable]`](https://creators.vrchat.com/worlds/udon/networking/events/#rate-limiting) @@ -95,8 +95,8 @@ else. Both fire on every client, hook before event, for the matching transition: ## How non-owner clients detect transitions -The owner gets its lifecycle hooks called directly from `Process`'s own -`OnProcessStarted`/`Stopped`/`Completed`. A non-owner has no such direct call: it derives +The owner gets its lifecycle hooks called directly from +`OnProcessStarted`/`Stopped`/`Completed` in `Process`. A non-owner has no such direct call: it derives the same events by diffing synced state inside `OnDeserialization`, comparing the newly received running/paused flags against what it last observed. This diffing approach has two sharp edges worth knowing if you're relying on exact event sequencing: @@ -105,7 +105,7 @@ sharp edges worth knowing if you're relying on exact event sequencing: packet. That's treated the same as a fresh start: `OnTimerStarted` fires, and if the timer happens to already be paused, `OnTimerPaused` fires right after it in the same deserialization call. The joiner doesn't need to separately poll `_isPaused`. -- **A stop-then-restart lands in one packet.** VRChat's manual sync doesn't send +- **A stop-then-restart lands in one packet.** VRChat manual sync doesn't send immediately on `RequestSerialization`; it [waits until its own rate limit allows, then sends whatever the synced fields currently hold](https://creators.vrchat.com/worlds/udon/networking/network-details/#manual-synchronization). diff --git a/docs/tsvrc/players-tracking/auto-player-tracker.md b/docs/core/players-tracking/auto-player-tracker.md similarity index 92% rename from docs/tsvrc/players-tracking/auto-player-tracker.md rename to docs/core/players-tracking/auto-player-tracker.md index b7ae981..2ce08cc 100644 --- a/docs/tsvrc/players-tracking/auto-player-tracker.md +++ b/docs/core/players-tracking/auto-player-tracker.md @@ -15,14 +15,14 @@ ready](../how-to/wiring-a-ready-check) for a common pairing with `ReadyCheckProc ## Getting an instance -`AutoPlayerTracker` ships as one of [TsVRC's own default pool +`AutoPlayerTracker` ships as one of [the Core default pool entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** a `[WirePool]` field of this type just resolves, nothing to register in Configure first, and codegen calls `TsConstruct` for you at wire time. - **Manual:** skip pooling entirely. Drag the shipped `AutoPlayerTracker` prefab into your scene and call `TsConstruct` on it yourself before calling anything else on it. Skipping - construction leaves [`Process`'s cached local-player ID](../core-concepts/process) at + construction leaves the [cached `Process` local-player ID](../core-concepts/process) at its default, which makes `IsProcessOwner()` report `false` even for the actual owner. ## Usage @@ -75,13 +75,13 @@ still `false` for all of it, and every one of those events is dropped. By the ti at that instant is what captures everyone who's genuinely present. A player who is still mid-join at that exact instant hasn't fired their own `OnPlayerJoined` yet. It fires afterward, once the process is live, and gets picked up normally through the -ordinary join path. Because UdonSharp runs on Unity's single main thread, no +ordinary join path. Because UdonSharp runs on the single Unity main thread, no `OnPlayerJoined` can interleave between reading that snapshot and the process actually going live, so there's no window where a joining player is missed by both paths. The snapshot itself excludes any player already suspended at that instant. A player who's suspended before tracking even starts has no suspend *transition* left to -produce, and `PlayerTracker`'s own suspend-handling only reacts to that transition, so a +produce, and the suspend-handling in `PlayerTracker` only reacts to that transition, so a pre-suspended player must never enter the tracked set in the first place. ## Events @@ -91,4 +91,4 @@ Same six lifecycle names as `PlayerTracker`, renamed with an `Auto` prefix `...PlayersAdded`, `...PlayersRemoved`) and their matching `OnAutoTracking*Event` constants. Each is just the base `OnTracking*` hook overridden to emit the renamed event. The underlying data (`LastPlayerIds`, `LastAddedPlayerIds`, -`LastRemovedPlayerIds`) is identical to `PlayerTracker`'s own. +`LastRemovedPlayerIds`) is identical to that of `PlayerTracker`. diff --git a/docs/tsvrc/players-tracking/head-clip-guard.md b/docs/core/players-tracking/head-clip-guard.md similarity index 96% rename from docs/tsvrc/players-tracking/head-clip-guard.md rename to docs/core/players-tracking/head-clip-guard.md index da5c03d..6f0b7b3 100644 --- a/docs/tsvrc/players-tracking/head-clip-guard.md +++ b/docs/core/players-tracking/head-clip-guard.md @@ -17,7 +17,7 @@ walls](../how-to/guarding-against-head-clipping) for a task-oriented walkthrough ## Getting an instance -`HeadClipGuard` ships as one of [TsVRC's own default factory +`HeadClipGuard` ships as one of [the Core default factory entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** `_ts.CreateHeadClipGuard(parent)` just works, nothing to register in @@ -36,7 +36,7 @@ guard.Begin(solidColliders, count); guard.End(); ``` -`Begin` bakes each `BoxCollider`'s world-space center, rotation, and half-extents once +`Begin` bakes the world-space center, rotation, and half-extents of each `BoxCollider` once (a one-time cost, not repeated per frame) and starts the guard. Null entries in the array are skipped rather than rejected. `End` fully resets all runtime state and is safe to call before a later `Begin` on the same instance, so one `HeadClipGuard` can be reused diff --git a/docs/tsvrc/players-tracking/player-color-assigner.md b/docs/core/players-tracking/player-color-assigner.md similarity index 96% rename from docs/tsvrc/players-tracking/player-color-assigner.md rename to docs/core/players-tracking/player-color-assigner.md index 50a87eb..cf10b3a 100644 --- a/docs/tsvrc/players-tracking/player-color-assigner.md +++ b/docs/core/players-tracking/player-color-assigner.md @@ -7,7 +7,7 @@ sidebar_position: 5 # PlayerColorAssigner `Tsvrc.Player.PlayerColorAssigner` assigns every player a stable, visually distinct -color from an 82-entry palette, one slot per player up to [VRChat's absolute maximum +color from an 82-entry palette, one slot per player up to [the absolute maximum VRChat instance size](https://wiki.vrchat.com/wiki/Special:MyLanguage/Instances) (80 is the configurable hard cap; the world author and instance creator can each still join on top of that, for 82 in the worst case), keyed off their numeric player ID so every client diff --git a/docs/tsvrc/players-tracking/player-tracker.md b/docs/core/players-tracking/player-tracker.md similarity index 96% rename from docs/tsvrc/players-tracking/player-tracker.md rename to docs/core/players-tracking/player-tracker.md index 6475716..c725f94 100644 --- a/docs/tsvrc/players-tracking/player-tracker.md +++ b/docs/core/players-tracking/player-tracker.md @@ -15,14 +15,14 @@ players](../how-to/tracking-a-group-of-players) for a task-oriented walkthrough. ## Getting an instance -`PlayerTracker` ships as one of [TsVRC's own default pool +`PlayerTracker` ships as one of [the Core default pool entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** a `[WirePool]` field of this type just resolves, nothing to register in Configure first, and codegen calls `TsConstruct` for you at wire time. - **Manual:** skip pooling entirely. Drag the shipped `PlayerTracker` prefab into your scene and call `TsConstruct` on it yourself before calling anything else on it. Skipping - construction leaves [`Process`'s cached local-player ID](../core-concepts/process) at + construction leaves the [cached `Process` local-player ID](../core-concepts/process) at its default, which makes `IsProcessOwner()` report `false` even for the actual owner. ## Usage @@ -97,7 +97,7 @@ covering a different way a tracked player can stop being reachable: to suspended; waking up needs no action, since the player was already removed on the way in. - **`OnOwnerAbandonedProcess`** — covers the case where the *tracker's owner itself* leaves - or suspends. The new owner (promoted by `Process`'s own ownership recovery) scans the + or suspends. The new owner (promoted by the `Process` ownership recovery) scans the entire tracked set against the current, non-suspended player list and removes anyone who's gone or suspended. This is what catches a tracked player who suspended while the process owner was the one who couldn't act on it (all non-owner clients skip that diff --git a/docs/tsvrc/players-tracking/ready-check-process.md b/docs/core/players-tracking/ready-check-process.md similarity index 95% rename from docs/tsvrc/players-tracking/ready-check-process.md rename to docs/core/players-tracking/ready-check-process.md index ed7bcb4..38df76e 100644 --- a/docs/tsvrc/players-tracking/ready-check-process.md +++ b/docs/core/players-tracking/ready-check-process.md @@ -14,14 +14,14 @@ walkthrough. ## Getting an instance -`ReadyCheckProcess` ships as one of [TsVRC's own default pool +`ReadyCheckProcess` ships as one of [the Core default pool entries](../explanations/builtin-registrations) (`TsBuiltinConfig`). - **Default:** a `[WirePool]` field of this type just resolves, nothing to register in Configure first, and codegen calls `TsConstruct` for you at wire time. - **Manual:** skip pooling entirely. Drag the shipped `ReadyCheckProcess` prefab into your scene and call `TsConstruct` on it yourself before calling anything else on it. Skipping - construction leaves [`Process`'s cached local-player ID](../core-concepts/process) at + construction leaves the [cached `Process` local-player ID](../core-concepts/process) at its default, which makes `IsProcessOwner()` report `false` even for the actual owner. ## Usage @@ -61,7 +61,7 @@ public class LoadingGate : TsBehaviour ## Why SetReady doesn't just check IsProcessRunning `SetReady` gates on an internal `_readyCheckActive` flag instead of `IsProcessRunning()`. -The reason is a real ordering gap: VRChat's manual sync only serializes and delivers +The reason is a real ordering gap: VRChat manual sync only serializes and delivers synced fields once its own rate limit allows, on a timeline independent of discrete network events, so calling `SetReady` from inside a network event handler can run before the synced `_isRunning` field has actually arrived, even though the corresponding @@ -93,7 +93,7 @@ individual ready/unready changes the way there is for the tracked set, so otherwise kept current. `SetReady` mutates directly when called on the owner and forwards to the owner via `BroadcastAddReadyPlayer`/`BroadcastRemoveReadyPlayer` (each `[NetworkCallable]`, rate-limited to 2/second: VRChat [recommends keeping -caller-invocable methods' rate as low as the use case +caller-invocable methods rate as low as the use case allows](https://creators.vrchat.com/worlds/udon/networking/events/#rate-limiting), and toggling ready a couple of times per check is all that's ever actually needed) otherwise. Both broadcast targets check that the caller is only ever marking *themselves* ready or @@ -120,7 +120,7 @@ already validated the target player through other means. untracked player's ready status would otherwise never actually influence completion, with no indication why. - Removing several tracked players at once (`OnTrackingPlayersRemoved`, which can remove - up to 79 players in one call via `OnOwnerAbandonedProcess`, one below [VRChat's 80-player + up to 79 players in one call via `OnOwnerAbandonedProcess`, one below [the VRChat 80-player hard cap per instance](https://wiki.vrchat.com/wiki/Special:MyLanguage/Instances)) batches the corresponding ready-list removal into one pass and one array allocation rather than one diff --git a/docs/tsvrc/players-tracking/ts-player.md b/docs/core/players-tracking/ts-player.md similarity index 88% rename from docs/tsvrc/players-tracking/ts-player.md rename to docs/core/players-tracking/ts-player.md index aec4754..3dab0ae 100644 --- a/docs/tsvrc/players-tracking/ts-player.md +++ b/docs/core/players-tracking/ts-player.md @@ -7,7 +7,7 @@ sidebar_position: 1 # TsPlayer `Tsvrc.Player.TsPlayer` is a static helper class for identifying and looking up players. -It has no dependency on any other TsVRC type, despite being used throughout the +It has no dependency on any other Core type, despite being used throughout the framework: by [`Process`](../core-concepts/process), the tracking chain ([`PlayerTracker`](./player-tracker), [`AutoPlayerTracker`](./auto-player-tracker), [`ReadyCheckProcess`](./ready-check-process)), the overlay UI @@ -17,18 +17,18 @@ framework: by [`Process`](../core-concepts/process), the tracking chain ## Why a custom player ID exists -VRChat's own `VRCPlayerApi.playerId` only identifies a player while they're present in +The VRChat `VRCPlayerApi.playerId` only identifies a player while they're present in the current instance: [`GetPlayerById`](https://creators.vrchat.com/worlds/udon/players/getting-players/#getplayerbyid) only resolves someone who's still around, and playerId numbering isn't guaranteed to line up the same way for every client watching the instance. Neither is safe to store -and compare later. TsVRC's own ID format, `displayName#playerId` (built by +and compare later. The Core ID format, `displayName#playerId` (built by `GetPlayerID`), is what gets stored and compared across clients throughout the framework instead. ## Usage -Store `GetPlayerID`'s result anywhere you need to refer to a player later, and resolve it +Store the result of `GetPlayerID` anywhere you need to refer to a player later, and resolve it back to a live `VRCPlayerApi` with `FindPlayerByID` (which returns `null` once that player has left): @@ -48,7 +48,7 @@ if (winner != null) - **`GetNumericPlayerId(string)`** — extracts the numeric ID from a `GetPlayerID` string, by scanning backward from the end for digits until a `#` or a non-digit. It's a manual parse rather than `int.Parse`, matching the framework's own preference for avoiding - standard-library calls that aren't guaranteed to be supported in Udon's more limited + standard-library calls that aren't guaranteed to be supported in the more limited Udon runtime. A malformed string (no `#` at all, a `#` with nothing after it, or a non-digit character right after the last `#`) returns `0` rather than throwing. A display name that itself contains `#` characters is handled correctly: parsing works @@ -72,6 +72,6 @@ if (winner != null) `GetNumericPlayerId("First#0")` returns `0`, the same value used to signal "no valid numeric ID found." [`Process.IsProcessOwner()`](../core-concepts/process#ownership-and-authority) accounts for this ambiguity explicitly by also checking that the ID isn't `0` before -treating it as a real match, since VRChat's own player IDs start at `1`; code that calls +treating it as a real match, since VRChat player IDs start at `1`; code that calls `GetNumericPlayerId` directly should apply the same caution rather than treating `0` as an ordinary valid ID. diff --git a/docs/core/testing/testing-your-world.md b/docs/core/testing/testing-your-world.md new file mode 100644 index 0000000..a7376f4 --- /dev/null +++ b/docs/core/testing/testing-your-world.md @@ -0,0 +1,43 @@ +--- +id: testing-your-world +title: Testing your world +sidebar_position: 1 +--- + +# Testing your world + +Test a world built on Core with [Udon Test Kit](/docs/udon-test-kit/intro), which runs Play Mode +tests in a real ClientSim session and works around the VRChat SDK problems that break them. Core +depends on the kit, so it's already in your project, and Core has no testing helpers of its own: +players, ownership, saved data, spies and reaching private fields all come from the kit. + +This page covers what's particular to a world built on Core: how a test builds your behaviours, +and what the Core codegen does while tests run. See [Set up automated +testing](../how-to/set-up-automated-testing) to add the assemblies to your project, and [Write your +first automated test](../how-to/writing-your-first-test) for a worked example. + +## Your behaviours in a test + +A test adds your behaviour to a GameObject with `AddComponent` and constructs it with +`TsConstruct`, the call your generated root makes at world startup. A behaviour that doesn't reach +`_ts` during the test can take `null`: + +```csharp +var referee = new GameObject("RoundReferee").AddComponent(); +referee.TsConstruct((TsRoot)null); +``` + +`TsConstruct` runs the behaviour's `TsStart` once, as at startup. An `AddComponent`-created +behaviour is plain C#, never compiled to Udon, so Unity doesn't call its `Start()` and the generated +startup sequence never runs for it; the test calls what it needs directly. + +Give the test class a [scene of its own](/docs/udon-test-kit/how-to/running-tests-in-their-own-scene), +and everything a test created is destroyed when it ends. + +## Codegen during a test run + +A test that creates or changes scene objects would otherwise trigger the reactive Core codegen, +which could regenerate your project's real output against the test's temporary scene. +[`TsGenerator`](../codegen/internals/ts-generator) skips every automatic trigger while tests run, +from the Test Runner window or the command line, and while the Editor enters Play Mode, so there's +nothing to set up for it. diff --git a/docs/tsvrc/ui-components/list/list-item.md b/docs/core/ui-components/list/list-item.md similarity index 94% rename from docs/tsvrc/ui-components/list/list-item.md rename to docs/core/ui-components/list/list-item.md index c6cb3ee..64d8fe2 100644 --- a/docs/tsvrc/ui-components/list/list-item.md +++ b/docs/core/ui-components/list/list-item.md @@ -42,7 +42,7 @@ pooled-but-currently-unbound instance. ## Wiring selection -Wire `_OnItemPressed` to a `Button`'s `onClick`. It's a no-op if the item isn't currently +Wire `_OnItemPressed` to the `onClick` of a `Button`. It's a no-op if the item isn't currently bound (guards against a stray press on a pooled item mid-transition). Otherwise it calls back into the owning list's `OnItemSelected(dataIndex)`, which is what actually records the selection and emits `TsvrcList.OnItemSelectedEvent`. `ListItem` itself never emits that event diff --git a/docs/tsvrc/ui-components/list/tsvrc-list.md b/docs/core/ui-components/list/tsvrc-list.md similarity index 92% rename from docs/tsvrc/ui-components/list/tsvrc-list.md rename to docs/core/ui-components/list/tsvrc-list.md index 6003b14..a1e170e 100644 --- a/docs/tsvrc/ui-components/list/tsvrc-list.md +++ b/docs/core/ui-components/list/tsvrc-list.md @@ -15,7 +15,7 @@ walkthrough. ## Setup -Wire up in the inspector: `_itemContainer` (typically a `ScrollRect`'s `Content` transform) +Wire up in the inspector: `_itemContainer` (typically the `Content` transform of a `ScrollRect`) and `_itemPrefab` (a prefab carrying a `ListItem` component). Set `_pageSize` to a positive number to paginate, or `-1` to load every item on one page with no pagination at all. @@ -37,8 +37,8 @@ pagination controls (`NextPage()`/`PreviousPage()` call `SetPage` for you, and a either end of the range). Each list entry is expected to be a `DataDictionary` token; a non-dictionary entry at a given -index is silently treated as an empty dictionary rather than causing an error, so a -`ListItem`'s `_OnBind` override should be written defensively against missing keys either +index is silently treated as an empty dictionary rather than causing an error, so the +`_OnBind` override in a `ListItem` should be written defensively against missing keys either way. ## Selection diff --git a/docs/tsvrc/ui-components/overlay/player-marker-renderer.md b/docs/core/ui-components/overlay/player-marker-renderer.md similarity index 100% rename from docs/tsvrc/ui-components/overlay/player-marker-renderer.md rename to docs/core/ui-components/overlay/player-marker-renderer.md diff --git a/docs/tsvrc/ui-components/overlay/player-position-overlay.md b/docs/core/ui-components/overlay/player-position-overlay.md similarity index 98% rename from docs/tsvrc/ui-components/overlay/player-position-overlay.md rename to docs/core/ui-components/overlay/player-position-overlay.md index cd63f98..416dc2a 100644 --- a/docs/tsvrc/ui-components/overlay/player-position-overlay.md +++ b/docs/core/ui-components/overlay/player-position-overlay.md @@ -58,11 +58,11 @@ what makes `StartOverlay`/`StopOverlay`/`OnTrackingDeserialization` restarting t mid-flight safe: an old, now-superseded tick fires, sees a nonzero counter, and does nothing. -## Buffers are pre-sized to VRChat's own cap +## Buffers are pre-sized to the VRChat cap Every internal buffer (cached positions, headings, player IDs, live player references) is sized to 82, the [worst-case absolute maximum instance -size](https://wiki.vrchat.com/wiki/Special:MyLanguage/Instances) (80 is VRChat's +size](https://wiki.vrchat.com/wiki/Special:MyLanguage/Instances) (80 is the VRChat configurable hard cap; the world author and instance creator can each still join on top of that), allocated once, lazily, on first use, and reused every tick rather than reallocated. `_ResolveTrackedPlayers` is zero-allocation diff --git a/docs/tsvrc/ui-components/overlay/raster-player-marker-renderer.md b/docs/core/ui-components/overlay/raster-player-marker-renderer.md similarity index 97% rename from docs/tsvrc/ui-components/overlay/raster-player-marker-renderer.md rename to docs/core/ui-components/overlay/raster-player-marker-renderer.md index 1f47864..e5822d3 100644 --- a/docs/tsvrc/ui-components/overlay/raster-player-marker-renderer.md +++ b/docs/core/ui-components/overlay/raster-player-marker-renderer.md @@ -11,7 +11,7 @@ sidebar_position: 3 texture displayed on a `RawImage`: a minimap-style overlay. It owns all the texture and pixel-buffer plumbing, the world-to-pixel coordinate mapping, and a flush-only-when-dirty optimization; you only override `DrawMarker` to decide what gets painted at each player's -projected pixel position, typically using [`TextureGraphics2D`](../texture-graphics-2d)'s +projected pixel position, typically using the [`TextureGraphics2D`](../texture-graphics-2d) drawing primitives. A separate public `MarkerShape` enum (`Circle`/`Triangle`) ships alongside this class for a subclass's own use when labeling marker styles; the class itself never reads it. diff --git a/docs/tsvrc/ui-components/texture-graphics-2d.md b/docs/core/ui-components/texture-graphics-2d.md similarity index 97% rename from docs/tsvrc/ui-components/texture-graphics-2d.md rename to docs/core/ui-components/texture-graphics-2d.md index 79054b1..631aed5 100644 --- a/docs/tsvrc/ui-components/texture-graphics-2d.md +++ b/docs/core/ui-components/texture-graphics-2d.md @@ -17,7 +17,7 @@ a minimap marker) is simpler than setting up a render pipeline for it. Every drawing operation comes in two forms, and neither one reaches the GPU by itself: - **Texture methods** (`DrawLine`, `DrawCircle`, `FillTexture`, ...) write straight into the - `Texture2D` you pass in, but only into its CPU-side pixel data. [Unity's own `SetPixel` + `Texture2D` you pass in, but only into its CPU-side pixel data. [The Unity `SetPixel` docs say so directly](https://docs.unity3d.com/ScriptReference/Texture2D.SetPixel.html): "you must call `Apply` after `SetPixel`" for a change to actually reach the GPU. None of these @@ -36,7 +36,7 @@ Use the buffer form whenever you're drawing more than one or two shapes before t needs to be visible: skipping repeated `SetPixel` calls in favor of plain array writes, and paying for [the genuinely expensive `Apply()`](https://docs.unity3d.com/ScriptReference/Texture2D.Apply.html) exactly once no -matter how many shapes you drew, is the entire reason the buffer family exists. Unity's own +matter how many shapes you drew, is the entire reason the buffer family exists. The Unity docs for `Apply` say why: it "copies all the pixels in the texture even if you've only changed some of the pixels," so the fewer times you call it, the better. @@ -44,7 +44,7 @@ changed some of the pixels," so the fewer times you call it, the better. Every method that takes explicit coordinates uses pixel coordinates with `(0, 0)` at the [bottom-left corner](https://docs.unity3d.com/ScriptReference/Texture2D.GetPixel.html) -("the lower left corner is (0, 0)," in Unity's own words), and clips silently at the +("the lower left corner is (0, 0)," as the Unity docs put it), and clips silently at the texture's or buffer's edges: a shape drawn partially or entirely out of bounds never throws, it just doesn't draw the out-of-bounds part. `FillTexture` and `ClearTexture` take no coordinates at all; they always cover the whole texture. @@ -80,7 +80,7 @@ it would with a real buffer in that situation. the four diagonal pixels are at distance `√2` from the center, which fails the `≤ 1` test. A negative radius draws nothing (the iteration range is empty rather than throwing). - **`DrawTriangleToBuffer`** draws a filled, direction-aware triangle inscribed in a - `(2×halfWidth) × (2×halfHeight)` box. `headingDegrees` follows Unity's Y-axis rotation + `(2×halfWidth) × (2×halfHeight)` box. `headingDegrees` follows the Unity Y-axis rotation convention: `0°` points the tip toward `+Y` (up), and increasing degrees rotate the tip clockwise (`90°` toward `+X`). Headings outside `[0, 360)`, including negative values, are accepted directly: trigonometric functions handle the wraparound, so `-90°` points the diff --git a/docs/tsvrc/utilities/mesh-combiner.md b/docs/core/utilities/mesh-combiner.md similarity index 97% rename from docs/tsvrc/utilities/mesh-combiner.md rename to docs/core/utilities/mesh-combiner.md index 9f35df9..415bd35 100644 --- a/docs/tsvrc/utilities/mesh-combiner.md +++ b/docs/core/utilities/mesh-combiner.md @@ -41,7 +41,7 @@ The visual mesh is built in two passes: sources are first merged per-material in meshes, then those per-material meshes are combined again as separate submeshes. This is what keeps material boundaries intact in the final multi-material result rather than losing per-source materials. A submesh index past the end of its source's material array reuses -that array's last material, matching how Unity's own `MeshRenderer` handles the same +that array's last material, matching how the Unity `MeshRenderer` handles the same mismatch. Colliders follow their own rules: - A **solid `MeshCollider`** merges into one combined collision mesh. @@ -81,7 +81,7 @@ letting a combine "succeed" and produce a broken or invisible result: Every combine groups its GameObject-level changes (the new GameObject, recreated collider children, source deactivation) under one Undo name, so a single Ctrl+Z reverses all of them at once rather than requiring several undo steps. The saved mesh asset(s) on disk aren't -part of that: Unity's `Undo` system doesn't track `AssetDatabase.CreateAsset`, so undoing a +part of that: the Unity `Undo` system doesn't track `AssetDatabase.CreateAsset`, so undoing a combine leaves the `.asset` file (and the `_Collider.asset` file, if one was created) in place. Delete those manually if you undo a combine you don't want to keep. diff --git a/docs/tsvrc/utilities/read-only-attribute.md b/docs/core/utilities/read-only-attribute.md similarity index 100% rename from docs/tsvrc/utilities/read-only-attribute.md rename to docs/core/utilities/read-only-attribute.md diff --git a/docs/tsvrc/utilities/ts-array.md b/docs/core/utilities/ts-array.md similarity index 96% rename from docs/tsvrc/utilities/ts-array.md rename to docs/core/utilities/ts-array.md index bd977f5..e5955ac 100644 --- a/docs/tsvrc/utilities/ts-array.md +++ b/docs/core/utilities/ts-array.md @@ -9,7 +9,7 @@ sidebar_position: 2 `Tsvrc.Utils.TsArray` is a static helper class for array operations that Udon can't express generically. UdonSharp doesn't support generic methods, so there's no single `Add`. Instead `TsArray` overloads each operation for `string[]` and `UdonSharpBehaviour[]`, the -two element types TsVRC itself needs this for. +two element types Core itself needs this for. ## What it's for @@ -27,7 +27,7 @@ arrays. order is preserved. - **`Contains(array, value)`** — returns whether `value` is present in `array`, compared by `==` (reference equality for `UdonSharpBehaviour[]`, value equality for `string[]`). For - `UdonSharpBehaviour[]`, that's [Unity's own overridden equality + `UdonSharpBehaviour[]`, that's [the overridden Unity equality operator](https://docs.unity3d.com/ScriptReference/Object-operator_eq.html), so a destroyed-but-not-yet-garbage-collected behaviour still correctly reports as equal to `null`, not as a dangling non-null reference. diff --git a/docs/tsvrc/utilities/ts-json.md b/docs/core/utilities/ts-json.md similarity index 92% rename from docs/tsvrc/utilities/ts-json.md rename to docs/core/utilities/ts-json.md index 130298c..eaa3496 100644 --- a/docs/tsvrc/utilities/ts-json.md +++ b/docs/core/utilities/ts-json.md @@ -6,9 +6,9 @@ sidebar_position: 3 # TsJson -`Tsvrc.Utils.TsJson` is a static helper class for converting VRChat's `DataDictionary` and +`Tsvrc.Utils.TsJson` is a static helper class for converting the VRChat `DataDictionary` and `DataToken` types to and from JSON, built on top of `VRCJson`. It exists to give the rest of -TsVRC a smaller, consistent surface than calling `VRCJson`'s `Try...` methods directly +Core a smaller, consistent surface than calling the `VRCJson` `Try...` methods directly everywhere a serialization step is needed. ## Methods @@ -36,7 +36,7 @@ if (restored != null) } ``` -Always check `Deserialize`'s result for `null` before reading from it: see the failure +Always check the result of `Deserialize` for `null` before reading from it: see the failure table below for why a bad string never throws instead. ## Failure handling diff --git a/docs/tsvrc/utilities/tsvrc-logger.md b/docs/core/utilities/tsvrc-logger.md similarity index 90% rename from docs/tsvrc/utilities/tsvrc-logger.md rename to docs/core/utilities/tsvrc-logger.md index ec1010e..83be820 100644 --- a/docs/tsvrc/utilities/tsvrc-logger.md +++ b/docs/core/utilities/tsvrc-logger.md @@ -6,11 +6,11 @@ sidebar_position: 4 # TsvrcLogger -`Tsvrc.Utils.TsvrcLogger` is TsVRC's centralized logging sink: a `TsvrcBehaviour` you reach +`Tsvrc.Utils.TsvrcLogger` is the centralized logging sink in Core: a `TsvrcBehaviour` you reach through `_ts.Log`, wrapping `Debug.Log`/`LogWarning`/`LogError` with a consistent message format and independent on/off toggles per severity. Its generated shadow class is -`TsLogger`. Most code never calls it directly: prefer `TsvrcBehaviour`'s own `LogInfo`, -`LogWarning`, and `LogError` methods, which supply the tag and context automatically. +`TsLogger`. Most code never calls it directly: prefer the `TsvrcBehaviour` methods `LogInfo`, +`LogWarning`, and `LogError`, which supply the tag and context automatically. ## Message format @@ -86,6 +86,6 @@ actually prints. ## Edge case worth knowing -Calling `Info`/`Warning`/`Error` on the logger directly, rather than through a -`TsvrcBehaviour`'s wrappers, means you supply your own `tag` string and `isInternal` value +Calling `Info`/`Warning`/`Error` on the logger directly, rather than through the +wrappers on a `TsvrcBehaviour`, means you supply your own `tag` string and `isInternal` value by hand: there's no automatic class-name tag or `IsTsvrcInternal` lookup at that layer. diff --git a/docs/install-a-package.md b/docs/install-a-package.md new file mode 100644 index 0000000..9df68f4 --- /dev/null +++ b/docs/install-a-package.md @@ -0,0 +1,40 @@ +--- +id: install-a-package +title: Install a TsVRC package +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Install a TsVRC package + +Every TsVRC package installs the same way. A project's own install page says which package to add, +what it needs, and how to check that it worked. + + + + +1. Add the TsVRC repository to the Creator Companion, once per computer: click + [Add to VCC](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json), or add + `https://vpm.tsvrc.com/index.json` as described in the VRChat + [Community Repositories](https://vcc.docs.vrchat.com/guides/community-repositories/) guide. +2. Click **Manage Project** next to your project, then **+** next to the package. + +The Creator Companion adds the package to your project's `Packages` folder, along with the packages +it depends on, and resolves the VRChat Worlds SDK version it needs. + + + + +1. Download the package's latest `.unitypackage` from the GitHub releases page its install page + links to. +2. With your project open, double-click the file, or use **Assets > Import Package > Custom + Package**, and import everything. + +The package goes in your project's `Packages` folder. A `.unitypackage` can't install the packages it +depends on or check your VRChat Worlds SDK version, so import those and check the version yourself. +In a project that uses the Creator Companion for other packages, use it for this one too: updates +and dependencies of a hand-imported package are yours to track. + + + diff --git a/docs/tsvrc/add-to-your-project.md b/docs/tsvrc/add-to-your-project.md deleted file mode 100644 index 240b55a..0000000 --- a/docs/tsvrc/add-to-your-project.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -id: add-to-your-project -title: Add TsVRC to your project -sidebar_position: 1 ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Add TsVRC to your project - -Get TsVRC into a Unity project either through the VRChat Creator Companion (VCC) or by -importing a `.unitypackage` by hand. VCC is strongly recommended: it resolves the VRChat -Worlds SDK version TsVRC depends on for you, something a `.unitypackage` import leaves -you to sort out yourself. - -## Requirements - -- **Unity 2022.3.** TsVRC's package manifest targets this version specifically. -- **A Unity world project with the [VRChat Worlds - SDK](https://creators.vrchat.com/worlds/) `3.10.x`.** VCC resolves this automatically - when you install TsVRC; importing the `.unitypackage` needs it already in your project. - -## Install the package - - - - -If TsVRC's repository isn't in your Creator Companion yet, add it first (a one-time -step, the same as adding any other [community -repository](https://vcc.docs.vrchat.com/guides/community-repositories/)): - -- **With VCC installed**, click [Add TsVRC to Creator - Companion](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json) - to open VCC directly to the add-repository prompt. -- **Or paste the listing URL manually**: open VCC, go to **Settings > Packages > Add - Repository**, and paste: - - ``` - https://vpm.tsvrc.com/index.json - ``` - -Once the repository's added: - -1. Open the Creator Companion, select your project, and click **Manage Project**. -2. Find **TsVRC** (`com.tsvrc.core`) in the package list and press the **+** next to it - to install it. -3. Wait for Unity to finish importing and recompiling. - - - - -1. Download the latest `.unitypackage` from [TsVRC's GitHub - Releases](https://github.com/tsvrc/tsvrc-core/releases). -2. With your project open, double-click the downloaded file (or use **Assets > Import - Package > Custom Package**) and import everything. -3. Wait for Unity to finish importing and recompiling. - -Nothing here resolves the VRChat Worlds SDK version for you, so confirm it's already at -`3.10.x` before importing. If your project also uses VCC for other packages, prefer the -VCC tab instead: mixing a hand-imported package with VCC-managed ones makes future -updates and dependency resolution your own responsibility to track. - - - - -You'll know it worked when Unity's menu bar gains a **Tsvrc** menu. Everything TsVRC does -from here runs from there. - -## What you just added - -TsVRC's manifest description sums it up: structured initialization, dependency wiring, -and editor codegen tooling for building VRChat worlds with UdonSharp. Concretely, the -package compiles as one runtime assembly (`Tsvrc.Runtime`), the base classes your world's -scripts extend and call into, and one editor-only assembly (`Tsvrc.Editor`) holding all -the codegen and Configure-window tooling. See [How TsVRC fits -together](./core-concepts/how-it-fits-together) for how those two halves relate. - -Three further assemblies, `Tsvrc.Testing.Framework`, `Tsvrc.Testing.Behaviours`, and -`Tsvrc.Testing.UI`, exist purely to support testing a *consumer's* own project, kept -separate from each other and from `Tsvrc.Runtime`/`Tsvrc.Editor` for the reasons covered -in [Testing your world](./testing/testing-your-world). - -## Next - -Continue to [Build your first behaviour](./first-behaviour) to initialize TsVRC in a -scene and get a script running. diff --git a/docs/tsvrc/how-to/set-up-automated-testing.md b/docs/tsvrc/how-to/set-up-automated-testing.md deleted file mode 100644 index 78d1f6f..0000000 --- a/docs/tsvrc/how-to/set-up-automated-testing.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -id: set-up-automated-testing -title: Set up automated testing -sidebar_position: 5 ---- - -# Set up automated testing - -Wire TsVRC's testing helpers into your project so you can write Play Mode and Edit Mode -tests against your own `UdonSharpBehaviour`s. - -## 1. Put your scripts and generated code in one assembly - -TsVRC's generated code declares the base types your world scripts extend (`TsBehaviour`, -`TsInstance`, and so on), and in turn references concrete types from your own scripts: a -Construct or Factory entry generates a field typed as whatever class you registered. That's -a two-way dependency, and Unity doesn't allow two assembly definitions to reference each -other. Give your project's own scripts folder a single `asmdef` placed high enough in the -folder tree to cover both your hand-written scripts and wherever your `TsConfig`'s -generated-folder setting points (they don't need to be nested inside each other, just both -under that one asmdef's root), referencing `Tsvrc.Runtime` plus whatever VRChat SDK/ -UdonSharp assemblies your scripts use. - -## 2. Add a test assembly - -Create an EditMode or PlayMode test assembly referencing your project's runtime asmdef, -`Tsvrc.Runtime`, and `Tsvrc.Testing.Framework` for the [reflection and ClientSim -helpers](../testing/testing-your-world). Add `Tsvrc.Testing.Behaviours` or -`Tsvrc.Testing.UI` too if your tests need the listener or list-item doubles they provide. - -## 3. Arm reactive codegen suppression for Play Mode tests - -A test that creates or mutates scene objects can otherwise trigger a real codegen -regenerate pass against your project's actual `TsConfig` mid-test-run. Add a one-line -`[SetUpFixture]` subclass to your PlayMode test assembly to hold that back for the whole -run: - -```csharp -[SetUpFixture] -public class AutomaticTriggersSetUpFixture : AutomaticTriggersSetUpFixtureBase { } -``` - -NUnit only discovers a `[SetUpFixture]` in the assembly it's physically compiled into, so -this needs adding once per test assembly. The base class living in -`Tsvrc.Testing.Framework` isn't enough by itself. - -With these three pieces in place, extend -[`TsPlayModeTestBase`](../testing/testing-your-world#play-mode-tests-tsplaymodetestbase) -to write a Play Mode test, or call `PrivateFieldAccess`'s static methods directly for an -Edit Mode one. diff --git a/docs/tsvrc/how-to/writing-your-first-test.md b/docs/tsvrc/how-to/writing-your-first-test.md deleted file mode 100644 index cf8888f..0000000 --- a/docs/tsvrc/how-to/writing-your-first-test.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -id: writing-your-first-test -title: Write your first automated test for your own behaviour -sidebar_position: 18 ---- - -# Write your first automated test for your own behaviour - -How to write a real Play Mode test against your own `TsvrcBehaviour` subclass, once -[the testing assemblies are wired into your project](./set-up-automated-testing), using -[`TsPlayModeTestBase`](../testing/testing-your-world#play-mode-tests-tsplaymodetestbase). - -## Steps - -1. Extend `TsPlayModeTestBase` and start ClientSim in a `[UnityTest]`: - - ```csharp - public class RoundRefereeTests : TsPlayModeTestBase - { - [UnityTest] - public IEnumerator RoundReferee_StartsOnBegin() - { - yield return StartClientSim(); - ``` - -2. Build your project's generated root from code instead of loading a saved scene, and - attach a fresh instance of the behaviour under test: - - ```csharp - var builder = BuildTsRoot(); - var referee = builder.WithNew("RoundReferee"); - builder.Build(); - ``` - - Any root field your test never touches (a Global, a Pool slot) is auto-filled with a - bare stand-in before `Build()` runs, so a generated stage that unconditionally - iterates every field of its module never throws on one your test doesn't care about. - -3. Drive the behaviour and assert on it: - - ```csharp - referee.BeginRound(); - - Assert.IsTrue(referee.IsProcessRunning()); - } - } - ``` - -## Reaching a private field or method - -Use [`PrivateFieldAccess`](../testing/testing-your-world) instead of making something -`public` just so a test can see it: - -```csharp -int remaining = PrivateFieldAccess.GetField(referee, "_remainingMilliseconds"); -``` - -## Asserting an event actually fired - -Wire a [`TsCallbackRecorder`](../testing/testing-your-world) as the subscriber instead of -writing a one-off listener class for every test: - -```csharp -var recorder = builder.WithNew("Recorder"); -referee.TsSubscribe(recorder, RoundReferee.OnRoundEndedEvent, nameof(TsCallbackRecorder.CallbackA)); - -// ...drive the round to completion... - -Assert.AreEqual(1, recorder.CallbackACount); -``` - -## Why this shape - -See [`Testing your world`](../testing/testing-your-world) for the full reasoning behind -building a root from code instead of a saved scene, the two Play Mode testing defects -patched automatically for any `TsPlayModeTestBase` subclass, and why the three testing -assemblies are split the way they are. diff --git a/docs/tsvrc/intro.md b/docs/tsvrc/intro.md deleted file mode 100644 index 6e211c8..0000000 --- a/docs/tsvrc/intro.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: intro -title: TsVRC -sidebar_position: 1 ---- - -# TsVRC - -TsVRC is an open-source, community-maintained framework for building VRChat worlds with -UdonSharp: structured initialization, dependency wiring, and an editor tool that generates -the code for both automatically (codegen), on top of the VRChat Worlds SDK. - -:::note -TsVRC is still pre-1.0. Everything documented here, including edge-case behavior on -reference pages, describes what the code does today, not a locked API contract. -::: - -## New to TsVRC? - -Start with [Add TsVRC to your project](./add-to-your-project), then -[Build your first behaviour](./first-behaviour): by the end you'll have a script running -in play mode. - -## Already using it? - -Jump straight to what you need: - -- **Doing a specific task** — [pooling a prefab](./how-to/pooling-a-prefab), - [showing player positions](./how-to/showing-player-positions), - [transferring data between clients](./how-to/transferring-data-between-clients), or - [wiring a ready check](./how-to/wiring-a-ready-check). -- **Looking up a type's behavior** — start from - [How TsVRC fits together](./core-concepts/how-it-fits-together) and follow its links, or - browse Reference in the sidebar, grouped by area: players, networking, UI, codegen, and - so on. -- **Understanding why TsVRC works the way it does** — - [Codegen instead of reflection](./explanations/codegen-vs-reflection) and the other pages - under Explanations. diff --git a/docs/tsvrc/testing/testing-your-world.md b/docs/tsvrc/testing/testing-your-world.md deleted file mode 100644 index 2396786..0000000 --- a/docs/tsvrc/testing/testing-your-world.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -id: testing-your-world -title: Testing your world -sidebar_position: 1 ---- - -# Testing your world - -TsVRC ships a small set of testing helpers your own project can use to write real, -ClientSim-backed Play Mode tests (and plain reflection-based Edit Mode tests) against your -own `UdonSharpBehaviour`s, without hand-rolling VRChat SDK setup and working around its Play -Mode quirks yourself. See [Set up automated testing](../how-to/set-up-automated-testing) -for how to add these assemblies to your project, and [Write your first automated -test](../how-to/writing-your-first-test) for a worked example; this page covers what -these assemblies give you. - -Nothing here does anything to your project just by being referenced. Every piece of -behavior only activates when you actually use it. `PrivateFieldAccess` is a set of plain -static methods with no base class requirement; `TsPlayModeTestBase` and its ClientSim -machinery only run for a test class that actually extends it. - -## Reflection helpers: PrivateFieldAccess - -`SetField`/`GetField`/`InvokeStatic`/`InvokeInstance` reach private fields and methods on -any object or static type from a test, useful for asserting on internal state a -behaviour's public surface doesn't expose. Standalone, works in either Edit Mode or Play -Mode. - -## Play Mode tests: TsPlayModeTestBase {/* #play-mode-tests-tsplaymodetestbase */} - -Extend `TsPlayModeTestBase` instead of setting up ClientSim by hand. It exposes `Players` -(a `ClientSimPlayerEnvironment`, for spawning, removing, or finding real `VRCPlayerApi` -instances in the test), `StartClientSim(...)` to begin a session, and -`BuildTsRoot()` to construct your project's generated composition root from code. - -```csharp -public class MyManagerTests : TsPlayModeTestBase -{ - [UnityTest] - public IEnumerator MyManager_DoesTheThing() - { - yield return StartClientSim(); - - var builder = BuildTsRoot(); - var myManager = builder.WithNew("MyManager"); - builder.Build(); - - myManager.DoTheThing(); - // Assert... - } -} -``` - -`BuildTsRoot` composes your generated root with `AddComponent` rather than loading a saved -scene, then drives the same `_TsLogStart`/`_TsMemoryStart`/`_TsGlobalStart`/`_TsPoolStart`/ -`_TsConstructStart`/`_TsInstanceStart` sequence a real client only gets from Unity -dispatching `Start()` on a scene-loaded object. This isn't a shortcut taken for convenience: -an `AddComponent`-created object is plain C#, never compiled to Udon bytecode, so there's no -VM gating `Start()`/`SendCustomEvent` dispatch the way there is for a real, saved scene's -baked-in Udon behaviours. Composing this way is what makes the sequence actually run at all -in a test. - -`WithNew(fieldName)` (used above) creates a fresh `T` and assigns it into the named root -field; `With(fieldName, instance)` assigns an instance you already built yourself instead. -Any root field neither call touched is auto-filled with a bare `AddComponent` stand-in the -moment `Build()` runs, so a generated stage that unconditionally iterates every field of its -module (`_TsGlobalStart` calling `TsConstruct` on every registered global, for example) never -throws a null reference on a field the test never cared about. `TsPlayModeTestBase` tracks -and tears down every GameObject the builder created, including those auto-filled -stand-ins, automatically. - -Two known VRChat SDK and Unity Test Framework Play Mode testing defects are patched -automatically for any class extending `TsPlayModeTestBase`. One, a `ClientSim` player-object -leak that piles up conflicting writers on the same file across back-to-back tests, goes -through `FixupRegistry`, which runs every registered `IPlayModeEnvironmentFixup`'s hooks at -the matching lifecycle point; if a future SDK version fixes it upstream, disable it with -`FixupRegistry.Disable()` rather than forking the testing framework itself. The -other, restoring Unity Test Framework's own Play Mode result reporting (which VRChat SDK's -event-listener filter otherwise strips), isn't registered there: it patches itself in via -its own domain-load-time static constructor the first time it detects a `TsPlayModeTestBase` -subclass anywhere in the project, before `FixupRegistry` or any test lifecycle hook ever -runs. `FixupRegistry.Disable()` has no effect on it, since it's never part of the -registry's own list to begin with. - -Stopping TsVRC's own reactive codegen from regenerating against a test's temporary scene -during a test run (see [`TsGenerator`](../codegen/internals/ts-generator)'s -`SuppressAutomaticTriggers`) requires reaching an `internal` editor API from outside the -package, granted once, package-wide, via an `InternalsVisibleTo` attribute, rather than -something you need to configure yourself. - -That suppression itself isn't automatic per test class the way the `TsPlayModeTestBase` -fixups are. It has to be armed once per test assembly, for the assembly's entire run, via -`AutomaticTriggersSetUpFixtureBase`. See [Set up automated -testing](../how-to/set-up-automated-testing) for the one-line subclass this requires. - -## Two companion assemblies, and why they're separate - -`Tsvrc.Testing.Framework` is a plain, editor-only C# library: none of its own types are meant -to be `AddComponent`'d as real Udon components. Two more pieces need to actually run as -`UdonSharpBehaviour`s, so they live in their own sibling assemblies instead: - -- **`Tsvrc.Testing.Behaviours`** provides `TsCallbackRecorder`, a generic `TsSubscribe`/ - `TsEmit` listener double that records how many times each callback fired (and, via an - optional shared log, cross-listener firing order). -- **`Tsvrc.Testing.UI`** provides `TestListItem` (a generic `ListItem` double) and - `TsvrcListTestBuilder.Build(...)`, which wires a bare `TsvrcList`'s private fields the same - way you'd otherwise have to by hand. - -The split exists because of an UdonSharp constraint: `AddComponent` only works for scripts -in an assembly registered as a U# assembly, and once registered, UdonSharp Udon-compiles -*every* source file in that assembly. Keeping the reflection/ClientSim helpers in a -separate, non-U#-registered assembly is what stops them from being dragged into a -compilation context they were never meant to run under. Some of them reach `internal` -editor APIs that Udon's compiler simply can't resolve. Add whichever companion assembly a -given test actually needs, alongside `Tsvrc.Testing.Framework`; none of the three depend on -each other beyond that. All three carry a `UNITY_INCLUDE_TESTS` define constraint, so none -of them compile at all outside a test context, regardless of platform: nothing here can ship -in a built world by accident. - -See [Set up automated testing](../how-to/set-up-automated-testing) for how to reference -these assemblies from your own project. diff --git a/docs/udon-test-kit/add-to-your-project.md b/docs/udon-test-kit/add-to-your-project.md new file mode 100644 index 0000000..e73fe71 --- /dev/null +++ b/docs/udon-test-kit/add-to-your-project.md @@ -0,0 +1,55 @@ +--- +id: add-to-your-project +title: Add Udon Test Kit to your project +sidebar_position: 2 +--- + +# Add Udon Test Kit to your project + +Install the kit in a VRChat world project, then create the test assemblies your tests go in. + +## Requirements + +- **Unity 2022.3.** +- **The [VRChat Worlds SDK](https://creators.vrchat.com/worlds/) 3.8.2 or later**, which includes + UdonSharp and ClientSim. +- **Unity Test Framework**, which new Unity projects already include. + +## Install the package + +Install **Udon Test Kit** (`com.tsvrc.udon-test-kit`) as described in +[Install a TsVRC package](/docs/install-a-package). Its `.unitypackage` is on the kit's +[GitHub releases](https://github.com/tsvrc/udon-test-kit/releases). + +You'll know it worked when the Unity menu bar has **Tools > TsVRC > Udon Test Kit**. + +:::warning +A project can hold only one copy of the kit. Two copies mean two assemblies named `Tsvrc.UdonTestKit`, +which stop the whole project compiling. Don't import the `.unitypackage` into a project that +already has the kit, including a copy another package installed as its dependency. +::: + +## Create your test assemblies + +Unity Test Framework finds tests in test assemblies, and the kit creates the three a world needs. + +1. In the Project window, select the folder your tests should live in, such as `Assets`. +2. Choose **Assets > Create > TsVRC > Udon Test Kit > Test Assemblies**. + +You get a `Tests` folder with three assemblies, each with an example that works: + +- `EditMode` and `PlayMode` hold your tests. +- `Doubles` holds behaviours your tests add to GameObjects, such as spies with callbacks of your + own. Both test assemblies reference it. + +They reference the kit, UdonSharp, the VRChat SDK, ClientSim and every assembly definition of yours +under `Assets`, and like the kit, they're left out of your world's builds. +[TestAssemblyCreator](./reference/test-assembly-creator) describes each one. + +If your scripts have no assembly definition, the menu item logs a warning, and Play Mode tests +can't reach them yet. See +[Test scripts that have no assembly definition](./how-to/testing-scripts-without-an-assembly-definition). + +## Next + +Continue to [Write your first test](./first-test) to run the examples and write a test of your own. diff --git a/docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md b/docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md new file mode 100644 index 0000000..c454694 --- /dev/null +++ b/docs/udon-test-kit/explanations/a-fresh-session-for-every-test.md @@ -0,0 +1,47 @@ +--- +id: a-fresh-session-for-every-test +title: "Decision note: a fresh session for every test" +sidebar_position: 2 +--- + +# Decision note: a fresh session for every test + +[`ClientSimTestBase`](../reference/clientsim-test-base) ends its ClientSim session after every +test, and the next test starts its own. A session ends completely: `End` leaves nothing of +ClientSim running. The one thing that carries over is saved data, and the kit never deletes it +unless a test asks. + +## Why tests don't share a session + +A test that starts from a fresh session can't depend on what an earlier test did, so each test +passes or fails on its own, in any order. NUnit doesn't promise an order between tests, so a test +that only passes after another one fails at random. In xUnit Test Patterns terms, this is a +fresh fixture. + +Ending a session completely also lets one test use more than one session. A test can end a session +and start another in the same scene to check that data survives a rejoin, without relying on the +order of tests. + +## Why saved data carries over + +Carrying over is what saved data is for, and ClientSim keeps it in files, not in the session. The +kit never deletes those files on its own. In your world's own scene, they're your ClientSim data, +and some worlds want them to carry over between tests. `ClientSimSession.DeleteSavedData` is there +for a test that needs none. + +## Alternatives considered + +- **One session for a whole test class** — faster, since ClientSim starts once, but every test + then shares whatever the earlier ones changed: players, owners, saved data and every object in + the scene. That's xUnit Test Patterns shared fixture, and its known cost is tests that depend on + each other. It stays possible for tests that only read, by starting the session in a + `[UnitySetUp]` when it isn't running yet and ending it in `[OneTimeTearDown]`. +- **Deleting saved data after every test** — every test would start clean, but tests run in the + world's own scene would delete the developer's ClientSim data, and a test of data carrying over + couldn't be written. + +## Scenes follow the same rule + +Every test in a Play Mode run shares the Unity Test Framework test scene by default. A +[`TestScene`](../reference/test-scene) with the default `PerTest` lifetime gives each test a fresh +copy of its own scene, and `PerFixture` is the shared option, for tests that don't change it. diff --git a/docs/udon-test-kit/explanations/what-the-kit-works-around.md b/docs/udon-test-kit/explanations/what-the-kit-works-around.md new file mode 100644 index 0000000..6d7d8f8 --- /dev/null +++ b/docs/udon-test-kit/explanations/what-the-kit-works-around.md @@ -0,0 +1,81 @@ +--- +id: what-the-kit-works-around +title: What the kit works around +sidebar_position: 1 +--- + +# What the kit works around + +Unity Test Framework, the VRChat SDK and ClientSim each work on their own. Put together in a Play +Mode test, a few of their behaviors break the test, or make it pass or fail for the wrong reason. +This page lists each one and what the kit does about it. + +## Play Mode runs that never finish + +When Play Mode starts, the VRChat SDK `UnityEventFilter` strips every persistent UnityEvent +listener whose type it doesn't allow, and a stripped listener is never called. The Unity Test +Framework Play Mode runner reports results through listeners of four such types. Without them, a +Play Mode run never reports its results, and a batch-mode run never exits. + +The kit adds those four types to the SDK allowlist whenever the Editor loads it, before any test +runs. Nothing needs turning on. The allowlist is internal to the SDK, so if a future SDK moves it, +the kit logs a warning starting with `[UdonTestKit] UnityEventFilterAllowlist:` instead of failing. + +## Tests that fail without saying why + +ClientSim checks a handful of project settings as it starts. When the check fails, it opens its +settings window, which in batch mode logs errors, and Unity Test Framework fails a test on any +error log it didn't expect. The test fails, and nothing in the result names a setting. + +`ClientSimSession.Start` runs the same check first and fails the test with the names of the +failing settings and the menu item that fixes them. See +[ClientSimProjectSettings](../reference/clientsim-project-settings). + +## Saved data a finished session overwrites + +ClientSim keeps each player's saved data on a hidden object that its own teardown never destroys. +After ClientSim stops, that object keeps rewriting the player's save file. The next session in the +same scene reads the same file, and within a second the leftover object overwrites what the new +session saved, so a test of saved data passes or fails depending on timing. + +`ClientSimSession.End` destroys that object. + +## Saved data restored after ClientSim is ready + +ClientSim reports that it's ready before it restores the local player's saved data, and a +PlayerData change made before the restore is never saved. `ClientSimSession.Start` returns only +after the restore. + +## Owners that never change + +In VRChat, every networked GameObject has an owner that can change. ClientSim tracks an owner only +for GameObjects with `VRCObjectSync` or `VRCObjectPool`. The master owns every other one, and +`Networking.SetOwner` can't change that, so code that behaves differently for a player who isn't +the owner can't be tested. `ClientSimSession.SetOwner` gives any GameObject an owner of its own. + +ClientSim also hands a leaving player's GameObjects to the master only when it tracks their owner, +so an owner added without telling ClientSim would stay with a player who has left. The kit +registers the owner it adds the same way ClientSim registers a `VRCObjectSync`, so the master takes +over, as in VRChat. See [ClientSimSession](../reference/clientsim-session#ownership). + +## A test scene with no scene descriptor + +ClientSim refuses to start without a scene descriptor, and the Unity Test Framework test scene has +none. `ClientSimSession.Start` adds one when no loaded scene has one. + +VRChat Worlds SDK versions before 3.8.2 also stop Play Mode outright when the scene has no +descriptor, before any test code runs, so every Play Mode run hangs. Nothing in a test can work +around that, which is why the kit requires 3.8.2 or later. + +## A crash after every test passed + +`EditorSceneManager.LoadSceneInPlayMode`, which loads a scene asset into a Play Mode test, drops +the operation it loads with. When the garbage collector finalized that operation as Unity exited, +Unity crashed after every test had passed, so a command-line run reported failure. `TestScene` +finalizes it right after the scene loads, while that's still safe. + +## Test code in a world build + +The kit compiles only when Unity Test Framework includes tests (`UNITY_INCLUDE_TESTS`), and so do +the test assemblies it creates. A world build never includes them, so the kit can stay installed +in a world you upload. diff --git a/docs/udon-test-kit/first-test.md b/docs/udon-test-kit/first-test.md new file mode 100644 index 0000000..68af93c --- /dev/null +++ b/docs/udon-test-kit/first-test.md @@ -0,0 +1,100 @@ +--- +id: first-test +title: Write your first test +sidebar_position: 3 +--- + +# Write your first test + +Run the example tests, then write a Play Mode test with two players in a real ClientSim session and +an Edit Mode test with a spy. You need the test assemblies from +[Add Udon Test Kit to your project](./add-to-your-project). + +## Run the examples + +1. Open **Window > General > Test Runner**. +2. In the **PlayMode** tab, select `ExamplePlayModeTests` and click **Run Selected**. + +Unity enters Play Mode, ClientSim starts with a local player, the test spawns a second player, and +the test passes. + +If it fails before ClientSim starts, with a message naming project settings, run +**Tools > TsVRC > Udon Test Kit > Fix ClientSim Project Settings** once and run the test again. See +[Fix the project settings ClientSim rejects](./how-to/fixing-clientsim-project-settings). + +## Write a Play Mode test + +In your `Tests/PlayMode` folder, create `PlayerListTests.cs`: + +```csharp +using System.Collections; +using NUnit.Framework; +using Tsvrc.UdonTestKit; +using UnityEngine.TestTools; +using VRC.SDKBase; + +public class PlayerListTests : ClientSimTestBase +{ + [UnityTest] + public IEnumerator RemovePlayer_AfterSpawning_LeavesOnlyTheLocalPlayer() + { + yield return Session.Start(); + VRCPlayerApi remote = Session.SpawnRemotePlayer("Remote"); + + Session.RemovePlayer(remote); + + Assert.That(VRCPlayerApi.GetPlayerCount(), Is.EqualTo(1)); + Assert.That(ClientSimSession.FindPlayerByName("Remote"), Is.Null); + } +} +``` + +`ClientSimTestBase` gives the test a `Session` and ends it after the test. `Session.Start` returns +once the local player has joined, and `SpawnRemotePlayer` and `RemovePlayer` take effect before +they return, so the test can assert straight away. + +Run it from the **PlayMode** tab. It passes. + +## Write an Edit Mode test with a spy + +UdonSharp code often calls other behaviours by method name, with `SendCustomEvent`. A spy records +those calls. In your `Tests/EditMode` folder, create `SpyTests.cs`: + +```csharp +using NUnit.Framework; +using Tsvrc.UdonTestKit; + +public class SpyTests +{ + [Test] + public void SendCustomEvent_TwoSpies_RecordsBothInOrder() + { + using var calls = new CallLog(); + CallbackSpy first = calls.CreateSpy("First"); + CallbackSpy second = calls.CreateSpy("Second"); + + second.SendCustomEvent(nameof(CallbackSpy.CallbackB)); + first.SendCustomEvent(nameof(CallbackSpy.CallbackA)); + + Assert.That(calls, Is.EqualTo(new[] { "Second.CallbackB", "First.CallbackA" })); + } +} +``` + +Here the test makes the calls itself. In a real test, you pass the spy to your own code as the +behaviour it calls, and the log shows what your code called, in order. The `using` destroys both +spies when the test ends. + +Run it from the **EditMode** tab. It passes. + +## Where to go next + +To test your own behaviours, add them to GameObjects in a Play Mode test, drive them, and assert on +what they did. The how-to guides cover the usual next steps: + +- [Run tests in a scene of their own](./how-to/running-tests-in-their-own-scene), including your + world's scene. +- [Test what happens for a player who isn't the owner](./how-to/testing-a-player-who-isnt-the-owner). +- [Check that saved data survives a rejoin](./how-to/testing-saved-data). +- [Check the calls your code makes by name](./how-to/checking-calls-made-by-name). +- [Run tests from the command line](./how-to/running-tests-from-the-command-line). diff --git a/docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md b/docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md new file mode 100644 index 0000000..933ee17 --- /dev/null +++ b/docs/udon-test-kit/how-to/adding-your-worlds-own-setup.md @@ -0,0 +1,50 @@ +--- +id: adding-your-worlds-own-setup +title: Add your world's own test setup +sidebar_position: 6 +--- + +# Add your world's own test setup + +When every test needs the same setup, such as your world's scene, a running session and the +objects your tests look up, put it in a base class of your own that extends `ClientSimTestBase`, +and extend that in your test classes. + +```csharp +public abstract class MyWorldTestBase : ClientSimTestBase +{ + protected Scoreboard Board; + + protected MyWorldTestBase() : base(TestScene.FromAsset("Assets/Scenes/MyWorld.unity")) + { + } + + [UnitySetUp] + public IEnumerator StartWorld() + { + yield return Session.Start(); + Board = Object.FindObjectOfType(); + } +} + +public class ScoreboardTests : MyWorldTestBase +{ + [UnityTest] + public IEnumerator AddPoint_ForLocalPlayer_ShowsOnePoint() + { + Board.AddPoint(); + yield return null; + + Assert.That(Board.PointsOf(Networking.LocalPlayer), Is.EqualTo(1)); + } +} +``` + +Use NUnit attributes in your base class as you would in a test class. NUnit runs a base class's +setup before yours and its teardown after yours, so with `ClientSimTestBase` underneath: + +- your `[UnitySetUp]` and `[SetUp]` run with the test scene already loaded and active, +- your `[TearDown]` runs while the session is still running, and +- your `[UnityTearDown]` runs after ClientSim has ended, but before the test scene unloads. + +[ClientSimTestBase](../reference/clientsim-test-base#lifecycle) lists the full order. diff --git a/docs/udon-test-kit/how-to/checking-calls-made-by-name.md b/docs/udon-test-kit/how-to/checking-calls-made-by-name.md new file mode 100644 index 0000000..611d0d5 --- /dev/null +++ b/docs/udon-test-kit/how-to/checking-calls-made-by-name.md @@ -0,0 +1,65 @@ +--- +id: checking-calls-made-by-name +title: Check the calls your code makes by name +sidebar_position: 4 +--- + +# Check the calls your code makes by name + +UdonSharp code often calls another behaviour by method name, with `SendCustomEvent`, on a target +and method name it stored earlier, such as a subscriber. Pass a +[`CallbackSpy`](../reference/callback-spy-and-call-log) as that target, and check what its +`CallLog` recorded. + +## Steps + +1. Create a log with `using var`, so its spies are destroyed when the test ends, and create a spy: + + ```csharp + using var calls = new CallLog(); + CallbackSpy listener = calls.CreateSpy("Listener"); + ``` + +2. Give the spy to your code as the target, with one of its callbacks as the method name: + + ```csharp + scoreboard.Subscribe(listener, nameof(CallbackSpy.CallbackA)); + ``` + +3. Act, then assert on the calls, in order: + + ```csharp + scoreboard.AddPoint(); + + Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); + ``` + +Spies created from the same log record into it in the order the calls arrive, so one assertion +covers several spies and the order between them. To ignore calls your setup caused, call +`calls.Clear()` before acting. + +## Callbacks with your own names + +When your code calls a fixed method name, such as `OnRoundEnded`, subclass `CallbackSpy` and call +`Record()` from a method with that name: + +```csharp +public class RoundListenerSpy : CallbackSpy +{ + public void OnRoundEnded() => Record(); +} +``` + +Create it with `calls.CreateSpy("Listener")`, and it records +`"Listener.OnRoundEnded"`. Put the subclass in your `Doubles` assembly, so both Edit Mode and Play +Mode tests can use it: Unity won't add a behaviour from the Edit Mode test assembly to a GameObject. + +## What to watch for + +Outside Udon, as in a test, a call by name reaches only a public method with no parameters and +exactly that name. A misspelled name does nothing and raises no error, so always assert on what the +log recorded, not just that nothing failed. + +`SendCustomEventDelayedSeconds` and `SendCustomEventDelayedFrames` never call anything outside +Udon. When the code under test delays a call that way, call the delayed method directly from the +test. diff --git a/docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md b/docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md new file mode 100644 index 0000000..8e3a25b --- /dev/null +++ b/docs/udon-test-kit/how-to/fixing-clientsim-project-settings.md @@ -0,0 +1,26 @@ +--- +id: fixing-clientsim-project-settings +title: Fix the project settings ClientSim rejects +sidebar_position: 9 +--- + +# Fix the project settings ClientSim rejects + +ClientSim checks a few project settings before it starts. When a Play Mode test fails before +ClientSim starts, with a message such as this one, your project fails that check: + +``` +The ClientSim project settings check fails on: input type, collision matrix. Fix them with Tools > TsVRC > Udon Test Kit > Fix ClientSim Project Settings. +``` + +## Steps + +1. Run **Tools > TsVRC > Udon Test Kit > Fix ClientSim Project Settings**. It changes each + setting that fails and logs which ones it changed. Settings that already pass are left alone. +2. Commit the changed files under `ProjectSettings`, so the tests pass on every machine that runs + them, including a build server. + +You only need to do this once per project, unless something changes those settings again. + +See [ClientSimProjectSettings](../reference/clientsim-project-settings) for what each setting is +and the change it gets. diff --git a/docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md b/docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md new file mode 100644 index 0000000..d8c0f96 --- /dev/null +++ b/docs/udon-test-kit/how-to/keeping-editor-tooling-out-of-test-runs.md @@ -0,0 +1,39 @@ +--- +id: keeping-editor-tooling-out-of-test-runs +title: Keep editor tooling from reacting to a test run +sidebar_position: 7 +--- + +# Keep editor tooling from reacting to a test run + +Editor tooling that reacts to domain reloads, asset imports or scene changes, such as a code +generator, also reacts to what a test run does: the reloads it causes, the test scene it creates, +the objects tests add. Have the tooling skip its work while tests run, by checking +[`TestRun.IsActive`](../reference/test-run). + +```csharp +[InitializeOnLoad] +internal static class MyGenerator +{ + static MyGenerator() + { + EditorApplication.hierarchyChanged += Regenerate; + } + + private static void Regenerate() + { + if (TestRun.IsActive || EditorApplication.isPlayingOrWillChangePlaymode) + { + return; + } + // ... + } +} +``` + +`TestRun.IsActive` is true from when a run reports its start until it reports its end, across the +domain reloads in between, and for the whole of a batch-mode process started with `-runTests`. A +Play Mode run reports its start only after the domain reload into Play Mode, so tooling that runs +on that reload also checks `EditorApplication.isPlayingOrWillChangePlaymode`. + +If your tooling's code is in an assembly definition, add `Tsvrc.UdonTestKit` to its references. diff --git a/docs/udon-test-kit/how-to/running-tests-from-the-command-line.md b/docs/udon-test-kit/how-to/running-tests-from-the-command-line.md new file mode 100644 index 0000000..b188263 --- /dev/null +++ b/docs/udon-test-kit/how-to/running-tests-from-the-command-line.md @@ -0,0 +1,53 @@ +--- +id: running-tests-from-the-command-line +title: Run tests from the command line +sidebar_position: 8 +--- + +# Run tests from the command line + +[`Invoke-UnityTests.ps1`](../reference/invoke-unity-tests) runs a Unity project's tests from +PowerShell on Windows and prints only what needs your attention: the tests that didn't pass, with +their messages, and a count of the rest. It's a generic script, independent of the kit: copy it +into any Unity project to use it there, whether or not that project has the kit. + +## Steps + +1. Get the script: `Scripts~/Invoke-UnityTests.ps1` in the + [kit's repository](https://github.com/tsvrc/udon-test-kit). If you installed the kit through the + Creator Companion, it's also in `Packages/com.tsvrc.udon-test-kit/Scripts~`. A `.unitypackage` + can't carry it. +2. Copy it into your project, such as its root folder. It finds the project by looking up from its + own folder. +3. Close the Unity Editor. Batch mode can't open a project another Editor has open. +4. Run it from that folder: + + ```powershell + .\Invoke-UnityTests.ps1 + ``` + +It runs your project's Edit Mode tests, then its Play Mode tests, each in its own batch-mode Unity +process, and exits with 1 when a test failed or was inconclusive, or a run didn't finish. That makes +it usable as a build step as it is. + +## Run part of the tests + +Run one mode: + +```powershell +.\Invoke-UnityTests.ps1 -TestMode PlayMode +``` + +Run particular test assemblies, separated with `;`: + +```powershell +.\Invoke-UnityTests.ps1 -AssemblyNames "MyWorld.Tests.EditMode;MyWorld.Tests.PlayMode" +``` + +By default, the script runs the tests in every assembly definition under `Assets` and in scripts +without one, and leaves out the tests that come with packages. The +[reference page](../reference/invoke-unity-tests) lists every option, including where it looks for +Unity and where it writes results. + +If PowerShell refuses to run the script because of its execution policy, allow local scripts once +with `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. diff --git a/docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md b/docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md new file mode 100644 index 0000000..51970b1 --- /dev/null +++ b/docs/udon-test-kit/how-to/running-tests-in-their-own-scene.md @@ -0,0 +1,51 @@ +--- +id: running-tests-in-their-own-scene +title: Run tests in a scene of their own +sidebar_position: 1 +--- + +# Run tests in a scene of their own + +By default, every Play Mode test runs in the Unity Test Framework test scene, which the whole run +shares, so whatever one test leaves behind is still there in the next. Give a test class a scene of +its own by passing a [`TestScene`](../reference/test-scene) to `ClientSimTestBase`. + +## An empty scene for every test + +```csharp +public class InventoryTests : ClientSimTestBase +{ + public InventoryTests() : base(TestScene.Empty("Inventory")) + { + } +} +``` + +Before your own setup runs, every test gets a new empty scene, which becomes the active scene. +ClientSim and the objects your test creates go into it, and it's unloaded after the test, +destroying all of them. + +## Your world's own scene + +```csharp +public class LobbyTests : ClientSimTestBase +{ + public LobbyTests() : base(TestScene.FromAsset("Assets/Scenes/MyWorld.unity"), TestSceneLifetime.PerFixture) + { + } +} +``` + +The scene doesn't need to be in the build settings. When it has a scene descriptor, the session +uses it, and the local player spawns at its spawn point. + +Choose the lifetime by what your tests do to the scene: + +- `TestSceneLifetime.PerTest`, the default, loads the scene again for every test, so each test + starts from the scene as saved. +- `TestSceneLifetime.PerFixture` loads it once for the whole class. A large scene loads faster + that way, but every test sees what the earlier ones changed, so keep it for tests that only read. + +ClientSim keeps saved data per scene name, so tests in your world's scene read and write the same +saved data you get when you press Play. To start a test from none, see +[Check that saved data survives a rejoin](./testing-saved-data). diff --git a/docs/udon-test-kit/how-to/sharing-a-session-between-tests.md b/docs/udon-test-kit/how-to/sharing-a-session-between-tests.md new file mode 100644 index 0000000..5f2afb0 --- /dev/null +++ b/docs/udon-test-kit/how-to/sharing-a-session-between-tests.md @@ -0,0 +1,50 @@ +--- +id: sharing-a-session-between-tests +title: Share one session between the tests in a class +sidebar_position: 5 +--- + +# Share one session between the tests in a class + +Starting ClientSim for every test is what keeps tests independent, but it takes time. For tests +that only read, such as several checks on the same starting state, one session can serve the whole +class. + +`ClientSimTestBase` ends its session after every test, so don't extend it here. Create a +[`ClientSimSession`](../reference/clientsim-session) yourself, start it in the first `[UnitySetUp]`, +and end it in `[OneTimeTearDown]`: + +```csharp +public class StartingStateTests +{ + private readonly ClientSimSession _session = new ClientSimSession(); + private bool _started; + + [UnitySetUp] + public IEnumerator StartSessionOnce() + { + if (_started) + { + yield break; + } + _started = true; + yield return _session.Start(); + } + + [OneTimeTearDown] + public void EndSession() + { + _session.End(); + } +} +``` + +NUnit uses one instance of the class for all of its tests, so `_started` stays true after the +first test. Unity Test Framework 1.1 has no setup that runs once per class and can wait for frames, +which is why the start goes in `[UnitySetUp]`. + +Tests that share a session also share whatever the earlier ones changed: players, owners, saved +data and objects in the scene. A test that changes any of them belongs in a class with a fresh +session per test. See +[Decision note: a fresh session for every test](../explanations/a-fresh-session-for-every-test) +for the tradeoff. diff --git a/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md b/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md new file mode 100644 index 0000000..12e0d6b --- /dev/null +++ b/docs/udon-test-kit/how-to/testing-a-player-who-isnt-the-owner.md @@ -0,0 +1,50 @@ +--- +id: testing-a-player-who-isnt-the-owner +title: Test what happens for a player who isn't the owner +sidebar_position: 2 +--- + +# Test what happens for a player who isn't the owner + +In ClientSim, the master owns every GameObject without `VRCObjectSync` or `VRCObjectPool`, and +`Networking.SetOwner` can't change that. The local player is the master by default, so code that +behaves differently for a player who doesn't own a GameObject never takes that path in a test. +[`Session.SetOwner`](../reference/clientsim-session#ownership) gives the GameObject another owner. + +## Steps + +1. Start the session and spawn a remote player: + + ```csharp + yield return Session.Start(); + VRCPlayerApi remote = Session.SpawnRemotePlayer("Remote"); + ``` + +2. Make the remote player the owner of the GameObject your behaviour is on: + + ```csharp + Session.SetOwner(remote, door.gameObject); + ``` + +3. Call your behaviour as the local player, who now doesn't own it, and assert on what it did: + + ```csharp + door.Open(); + + Assert.That(door.IsOpen, Is.False); + ``` + +After `Session.SetOwner`, `Networking.SetOwner` works on that GameObject too. If the code under +test takes ownership before acting, check that it did: + +```csharp +door.TakeOverAndOpen(); + +Assert.That(Networking.IsOwner(door.gameObject), Is.True); +``` + +## The local player as a non-master + +To test as a player who isn't the master, start the session with +`Session.Start(localPlayerIsMaster: false)`. ClientSim spawns a remote player before the local +one, and that player is the master. diff --git a/docs/udon-test-kit/how-to/testing-saved-data.md b/docs/udon-test-kit/how-to/testing-saved-data.md new file mode 100644 index 0000000..ab39040 --- /dev/null +++ b/docs/udon-test-kit/how-to/testing-saved-data.md @@ -0,0 +1,59 @@ +--- +id: testing-saved-data +title: Check that saved data survives a rejoin +sidebar_position: 3 +--- + +# Check that saved data survives a rejoin + +ClientSim saves each player's PlayerData and PlayerObjects to files in your project. Ending a +session stops it from saving, so the next session in the same scene starts from what the last one +saved, like a player who leaves and rejoins. That lets one test check that data survives a rejoin. + +## Steps + +1. Start a session and change the data: + + ```csharp + yield return Session.Start(); + PlayerData.SetString("coins", "10"); + ``` + +2. Wait a frame. ClientSim has saved the change by then: + + ```csharp + yield return null; + ``` + +3. End the session and start a new one, the rejoin: + + ```csharp + Session.End(); + yield return Session.Start(); + ``` + +4. Assert on what the new session restored: + + ```csharp + Assert.That(PlayerData.TryGetString(Networking.LocalPlayer, "coins", out string coins), Is.True); + Assert.That(coins, Is.EqualTo("10")); + ``` + +Keep the whole rejoin inside one test. NUnit doesn't promise an order between tests, so a test that +saves and a later test that reads would pass or fail depending on that order. + +## Starting from no saved data + +Saved data stays on disk after a test, so a later test in the same scene starts from it too. When a +test needs none, delete it before starting the session: + +```csharp +ClientSimSession.DeleteSavedData(); +yield return Session.Start(); +``` + +`DeleteSavedData` deletes what ClientSim saved for the active scene, for every player. In your +world's own scene, those files are also the ones you get when you press Play. + +Unity Test Framework names its test scene differently on every run, so each run leaves a few files +in `ClientSimStorage` that no later run reads. Delete them whenever you like. diff --git a/docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md b/docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md new file mode 100644 index 0000000..25d4a32 --- /dev/null +++ b/docs/udon-test-kit/how-to/testing-scripts-without-an-assembly-definition.md @@ -0,0 +1,33 @@ +--- +id: testing-scripts-without-an-assembly-definition +title: Test scripts that have no assembly definition +sidebar_position: 10 +--- + +# Test scripts that have no assembly definition + +Scripts without an assembly definition compile into `Assembly-CSharp`, which no assembly +definition can reference, including the test assemblies +[`TestAssemblyCreator`](../reference/test-assembly-creator) creates. Creating them logs a warning +when your project has such scripts. + +## Edit Mode tests + +Put the tests in any folder named `Editor` that has no assembly definition. Unity compiles them +into `Assembly-CSharp-Editor`, which can use your scripts, NUnit and the kit without any +references set up. The Unity Test Runner finds them there, and so does +[`Invoke-UnityTests.ps1`](../reference/invoke-unity-tests). + +## Play Mode tests + +Play Mode tests need an assembly definition of their own, so the scripts they test need one too: + +1. Create an assembly definition in the folder that holds your scripts (**Assets > Create > + Assembly Definition**). It covers the scripts in that folder and its subfolders. +2. Give it the references your scripts need, such as `UdonSharp.Runtime`, `VRC.SDKBase` and + `VRC.SDK3`. +3. Create a U# assembly definition next to it (**Assets > Create > U# Assembly Definition**) and + set its source assembly to the one you created, so UdonSharp keeps compiling those scripts for + Udon. +4. Add its name to the references of your Play Mode test assembly, or delete the test assemblies + and create them again, which picks it up. diff --git a/docs/udon-test-kit/intro.md b/docs/udon-test-kit/intro.md new file mode 100644 index 0000000..268961b --- /dev/null +++ b/docs/udon-test-kit/intro.md @@ -0,0 +1,57 @@ +--- +id: intro +title: Udon Test Kit +sidebar_position: 1 +--- + +# Udon Test Kit + +Udon Test Kit lets you write automated tests for a VRChat world with Unity Test Framework. Play +Mode tests run your UdonSharp behaviours in a real ClientSim session, with real players. + +## Why test your world + +An automated test is a small piece of code that runs part of your world and checks the result, +such as that a door opens only for the player who owns it. Unity runs these tests with its +[Unity Test Framework](https://docs.unity3d.com/Packages/com.unity.test-framework@1.1/manual/index.html), +either in the Editor (Edit Mode) or with the game running (Play Mode). + +Once written, tests run again whenever you ask, all at once. A change that breaks something shows +up right away, before you upload the world, and you can change your code without worrying about +breaking something you aren't looking at. That matters most for what's hardest to check by playing +on your own: several players, who owns what, and data that has to survive a rejoin. The Unity +[testing and quality assurance tips](https://unity.com/how-to/testing-and-quality-assurance-tips-unity-projects) +go into more depth. + +## What you can do with it + +- Spawn and remove players, and choose who is the master. +- Give any GameObject an owner, to test what a player who doesn't own it sees. +- Keep saved data between sessions, like a player who leaves and rejoins. +- Record the calls your code makes by name, such as with `SendCustomEvent`. +- Run each test in a scene of its own, empty or your world's. +- Create the test assemblies your world needs from one menu item. + +The kit only compiles for tests, so nothing from it ends up in your world's build. + +It also includes `Invoke-UnityTests.ps1`, a standalone PowerShell script that runs a project's +tests in one command and prints only what failed. It works in any Unity project, with or without +the kit. + +## Get started + +1. [Add Udon Test Kit to your project](./add-to-your-project) +2. [Write your first test](./first-test) + +## Common tasks + +- [Run tests in a scene of their own](./how-to/running-tests-in-their-own-scene) +- [Test what happens for a player who isn't the owner](./how-to/testing-a-player-who-isnt-the-owner) +- [Check that saved data survives a rejoin](./how-to/testing-saved-data) +- [Run tests from the command line](./how-to/running-tests-from-the-command-line) + +## Reference and background + +- [ClientSimSession](./reference/clientsim-session), where most tests start +- [What the kit works around](./explanations/what-the-kit-works-around) +- [Decision note: a fresh session for every test](./explanations/a-fresh-session-for-every-test) diff --git a/docs/udon-test-kit/reference/callback-spy-and-call-log.md b/docs/udon-test-kit/reference/callback-spy-and-call-log.md new file mode 100644 index 0000000..a2cd9f3 --- /dev/null +++ b/docs/udon-test-kit/reference/callback-spy-and-call-log.md @@ -0,0 +1,78 @@ +--- +id: callback-spy-and-call-log +title: CallbackSpy and CallLog +sidebar_position: 4 +--- + +# CallbackSpy and CallLog + +`Tsvrc.UdonTestKit.CallbackSpy` is a test spy for code that calls methods on an UdonSharpBehaviour by +name, such as with `SendCustomEvent`. `Tsvrc.UdonTestKit.CallLog` creates spies and records every call +they receive, in order, as `"GameObjectName.CallbackName"`. + +```csharp +using var calls = new CallLog(); +CallbackSpy listener = calls.CreateSpy("Listener"); +scoreboard.Subscribe(listener, nameof(CallbackSpy.CallbackA)); + +scoreboard.AddPoint(); + +Assert.That(calls, Is.EqualTo(new[] { "Listener.CallbackA" })); +``` + +Both work in Edit Mode and Play Mode tests. +[Check the calls your code makes by name](../how-to/checking-calls-made-by-name) walks through it. + +## CallLog + +`CallLog` is an `IReadOnlyList` of the calls so far, so NUnit constraints such as +`Is.EqualTo(new[] { ... })` and `Has.Member(...)` work on it directly. It's also `IDisposable`. + +- `CreateSpy(string name = "Spy")` creates a GameObject with that name in the active scene, adds a + `CallbackSpy` to it, and returns the spy. +- `CreateSpy(string name)` does the same with your own subclass of `CallbackSpy`. +- `Clear()` forgets the calls recorded so far, such as the ones a test's setup caused. It doesn't + destroy any spy. +- `Dispose()` destroys the GameObject of every spy it created, skipping any already destroyed. + `using var calls = new CallLog();` is all the cleanup a test needs. + +Spies created from the same log record into it in the order the calls arrive, so one assertion +checks both how many calls each spy got and their order across spies. + +## CallbackSpy + +`CallbackSpy` is an `UdonSharpBehaviour` with two callbacks, `CallbackA()` and `CallbackB()`, which +cover most tests. Each call is recorded under the spy's GameObject name at the time of the call. + +For callbacks named after what your code calls, subclass it and call `Record()` from each method. +`Record` takes the name of the method that called it: + +```csharp +public class DoorSpy : CallbackSpy +{ + public void Open() => Record(); + public void Close() => Record(); +} +``` + +`calls.CreateSpy("Door")` then records `"Door.Open"` and `"Door.Close"`. + +Put subclasses in an assembly that compiles for every platform, such as the `Doubles` assembly +[`TestAssemblyCreator`](./test-assembly-creator) creates. Unity won't add a behaviour from an +Editor-only assembly to a GameObject, and an Edit Mode test assembly is Editor-only. + +## Calls by name outside Udon + +Tests run your UdonSharpBehaviours as plain C#, not as Udon. In that mode, `SendCustomEvent` +calls a public method with no parameters and exactly the given name. Any other name does nothing +and raises no error, so a misspelled callback name passes silently: check what the log recorded, +not just that nothing failed. + +`SendCustomEventDelayedSeconds` and `SendCustomEventDelayedFrames` don't call anything outside +Udon. Code under test that relies on them needs its delayed method called directly from the test. + +## Failure modes + +- A `CallbackSpy` that a `CallLog` didn't create throws `InvalidOperationException` on its first + call, naming the spy and the callback. Add a spy with `CreateSpy`, not `AddComponent`. +- Spies record names only, never arguments. diff --git a/docs/udon-test-kit/reference/clientsim-project-settings.md b/docs/udon-test-kit/reference/clientsim-project-settings.md new file mode 100644 index 0000000..7aa01b0 --- /dev/null +++ b/docs/udon-test-kit/reference/clientsim-project-settings.md @@ -0,0 +1,50 @@ +--- +id: clientsim-project-settings +title: ClientSimProjectSettings +sidebar_position: 7 +--- + +# ClientSimProjectSettings + +`Tsvrc.UdonTestKit.ClientSimProjectSettings` knows the project settings ClientSim checks before it +starts, and the change that makes each one pass. Its menu item, +**Tools > TsVRC > Udon Test Kit > Fix ClientSim Project Settings**, applies the change for every +setting that fails. + +## The settings + +| Setting | Change it makes | +| --- | --- | +| Input axes | The ClientSim input axes, written over `ProjectSettings/InputManager.asset`. | +| Input type | **Active Input Handling** set to both input systems. | +| Audio spatializer | The ClientSim audio settings. | +| VRChat layers | The VRChat layers, through the VRChat SDK. | +| Collision matrix | The VRChat layer collision matrix, through the VRChat SDK. | + +All but one use the fix from ClientSim or the VRChat SDK. For the input type, the ClientSim +setter picks the Input System alone, which the ClientSim check then rejects, so the kit sets both +input systems, the value the ClientSim settings window asks for. + +## Members + +- `Fix()` is the menu item. It applies the change for each setting that fails the ClientSim check and + logs the ones it changed, or that they were already correct. Settings that pass are left alone. +- `FixMenuPath` is the menu item's path, + `"Tools/TsVRC/Udon Test Kit/Fix ClientSim Project Settings"`, for tooling that runs it with + `EditorApplication.ExecuteMenuItem`. + +## When it matters + +[`ClientSimSession.Start`](./clientsim-session) runs the ClientSim check before ClientSim starts. When +the check fails, `Start` fails the test with a message such as: + +``` +The ClientSim project settings check fails on: input type, collision matrix. Fix them with Tools > TsVRC > Udon Test Kit > Fix ClientSim Project Settings. +``` + +Without that, ClientSim would open its settings window as it starts. In batch mode, that window +logs errors, and Unity Test Framework fails a test on any error log it didn't expect, so the test +fails without naming any setting. + +The fix changes files under `ProjectSettings`. Commit them, so the check passes on every machine +that runs the tests, including a build server. diff --git a/docs/udon-test-kit/reference/clientsim-session.md b/docs/udon-test-kit/reference/clientsim-session.md new file mode 100644 index 0000000..a2d2477 --- /dev/null +++ b/docs/udon-test-kit/reference/clientsim-session.md @@ -0,0 +1,107 @@ +--- +id: clientsim-session +title: ClientSimSession +sidebar_position: 1 +--- + +# ClientSimSession + +`Tsvrc.UdonTestKit.ClientSimSession` starts ClientSim for a Play Mode test, gives the test real +`VRCPlayerApi` players, and ends ClientSim so that nothing of it is left running. +[`ClientSimTestBase`](./clientsim-test-base) holds one and ends it after every test; create one +yourself only when your test class doesn't extend it. + +```csharp +var session = new ClientSimSession(); +yield return session.Start(); + +VRCPlayerApi remote = session.SpawnRemotePlayer("Remote"); +// ... + +session.End(); +``` + +## Starting + +`Start(bool localPlayerIsMaster = true)` is a coroutine, so `yield return` it from a `[UnityTest]` +or a `[UnitySetUp]`. It returns once the local player has joined and, with player persistence on, +once that player's saved data is restored. ClientSim reports that it's ready before the restore, +and a PlayerData change made in between is never saved, so returning on "ready" alone would lose +the first thing a test writes. + +Before ClientSim starts, `Start` does two things: + +1. It runs the same project settings check ClientSim runs, and fails the test if it doesn't pass. + The failure message names each setting that fails and the menu item that fixes them, described + in [ClientSimProjectSettings](./clientsim-project-settings). +2. When no loaded scene has a scene descriptor, it adds one: a GameObject named + `__TestSceneDescriptor` with a single spawn point at the origin. ClientSim refuses to start + without a descriptor. When a scene already has one, such as your world's own scene, the + session adds nothing and the local player spawns at that descriptor's spawn point. + +With `localPlayerIsMaster: false`, ClientSim spawns a remote player before the local one, and +that remote player is the master. The session then starts with two players. + +If ClientSim isn't ready within 10 seconds, `Start` throws a `TimeoutException`. + +## Players + +`SpawnRemotePlayer(string displayName)` spawns a remote player and returns it. The player is in +the player list by the time the call returns, so a test can assert on it in the same frame. The +returned player is looked up by display name, so give every player its own. + +`RemovePlayer(VRCPlayerApi player)` removes a player, also within the call. + +`ClientSimSession.FindPlayerByName(string displayName)` is static and returns the player with that +display name, or `null` when there's none. + +## Ownership + +`SetOwner(VRCPlayerApi player, GameObject obj)` makes `player` the owner of `obj`, the way +`Networking.SetOwner` does in VRChat. + +ClientSim tracks an owner only for GameObjects with `VRCObjectSync` or `VRCObjectPool`. The master +owns every other GameObject, including one holding an UdonSharpBehaviour, and +`Networking.SetOwner` leaves it unchanged. In VRChat, every networked GameObject's owner can +change, so code that takes ownership can't be tested against ClientSim as it is. + +`SetOwner` gives such a GameObject an owner of its own. It adds a hidden component that ClientSim +reads and writes ownership through, starting from the GameObject's current owner, the master, as +in VRChat. Then it moves ownership to `player`. From then on, `Networking.SetOwner` calls on that +GameObject move its ownership too, including the ones the code under test makes. When the owner +leaves, through `RemovePlayer`, the master becomes the owner, as ClientSim does for `VRCObjectSync`. +Calling `SetOwner` again reuses the component instead of adding another. On a GameObject that +already has `VRCObjectSync` or `VRCObjectPool`, it only calls `Networking.SetOwner`. + +To test code that moves ownership itself while the local player still owns the GameObject, give +it its starting owner first with `SetOwner(Networking.LocalPlayer, obj)`. Without it, ClientSim +ignores the code's own `Networking.SetOwner` calls. +[Test what happens for a player who isn't the owner](../how-to/testing-a-player-who-isnt-the-owner) +shows it in a test. + +## Ending + +`End()` stops ClientSim, stops it saving player data, and removes the scene descriptor `Start` +added, if it added one. It's safe to call when no session is running, so a teardown can call it +unconditionally. + +ClientSim keeps each player's saved data on a hidden object that its own teardown never destroys. +That object keeps rewriting the player's save file after ClientSim stops, and overwrites whatever +a later session in the same scene saves. `End` destroys it. With nothing left running, the next +session in the same scene starts from the data the last one saved, the way a player who leaves and +rejoins does. + +## Saved data + +ClientSim saves each player's data in your project folder, one file per player and scene: + +- `ClientSimStorage/PlayerData/PlayerData__.json` +- `ClientSimStorage/PlayerObjects/PlayerObject__.json` + +`ClientSimSession.DeleteSavedData()` is static and deletes both kinds of file for the active scene, +for every player. Nothing else in the kit deletes saved data: in your world's own scene, those +files are your ClientSim data. [Check that saved data survives a rejoin](../how-to/testing-saved-data) +uses both. + +Unity Test Framework gives its test scene a new name on every run, so each run leaves a few files +in `ClientSimStorage` that no later run reads. You can delete them whenever you like. diff --git a/docs/udon-test-kit/reference/clientsim-test-base.md b/docs/udon-test-kit/reference/clientsim-test-base.md new file mode 100644 index 0000000..611cd3b --- /dev/null +++ b/docs/udon-test-kit/reference/clientsim-test-base.md @@ -0,0 +1,70 @@ +--- +id: clientsim-test-base +title: ClientSimTestBase +sidebar_position: 2 +--- + +# ClientSimTestBase + +`Tsvrc.UdonTestKit.ClientSimTestBase` is an abstract base class for Play Mode test fixtures that use +ClientSim. It holds a [`ClientSimSession`](./clientsim-session) in its protected `Session` field +and ends it after every test, and it can run every test in a [`TestScene`](./test-scene) of its +own. + +```csharp +public class PlayerCountTests : ClientSimTestBase +{ + [UnityTest] + public IEnumerator SpawnRemotePlayer_InClientSim_JoinsThePlayerList() + { + yield return Session.Start(); + + VRCPlayerApi remote = Session.SpawnRemotePlayer("Remote"); + + Assert.That(VRCPlayerApi.GetPlayerCount(), Is.EqualTo(2)); + } +} +``` + +The session doesn't start on its own. Each test starts it with `yield return Session.Start()`, so +a test can choose `localPlayerIsMaster: false`, or do work before ClientSim exists. + +## Constructors + +- `ClientSimTestBase()` runs every test in the Unity Test Framework test scene, which the whole + run shares. +- `ClientSimTestBase(TestScene scene, TestSceneLifetime lifetime = TestSceneLifetime.PerTest)` runs + every test in `scene`. With `PerTest`, the scene is loaded fresh for each test and unloaded after + it. With `PerFixture`, it's loaded for the first test and unloaded after the last one. + +Pass the scene from your fixture's own constructor: + +```csharp +public class LobbyTests : ClientSimTestBase +{ + public LobbyTests() : base(TestScene.Empty("Lobby")) + { + } +} +``` + +## Lifecycle + +The base class adds four NUnit and Unity Test Framework methods. NUnit runs a base class's setup +before the derived class's and its teardown after, and Unity Test Framework runs every +`[UnityTearDown]` after every `[TearDown]`. Put together, a test with a derived class runs in this +order: + +1. `[UnitySetUp]` `LoadTestScene` loads the scene, if there is one, and makes it the active scene. +2. Your `[UnitySetUp]` and `[SetUp]`. +3. The test, which starts the session. +4. Your `[TearDown]`, while the session is still running. +5. `[TearDown]` `EndClientSimSession` calls `Session.End()`. +6. Your `[UnityTearDown]`, after ClientSim has ended and before the scene unloads. +7. `[UnityTearDown]` `UnloadPerTestScene` unloads a `PerTest` scene, destroying everything in it. + +After the fixture's last test, `[OneTimeTearDown]` `UnloadPerFixtureScene` unloads a `PerFixture` +scene. + +`Session.End()` runs after every test, even when the test failed or never started the session, +because ending a session that isn't running does nothing. diff --git a/docs/udon-test-kit/reference/invoke-unity-tests.md b/docs/udon-test-kit/reference/invoke-unity-tests.md new file mode 100644 index 0000000..5da4c5d --- /dev/null +++ b/docs/udon-test-kit/reference/invoke-unity-tests.md @@ -0,0 +1,83 @@ +--- +id: invoke-unity-tests +title: Invoke-UnityTests.ps1 +sidebar_position: 9 +--- + +# Invoke-UnityTests.ps1 + +`Invoke-UnityTests.ps1` runs a Unity project's tests from PowerShell on Windows and prints a short +report. It's a single, standalone file: it doesn't use the kit or anything else beyond Unity Test +Framework, and it's meant to be copied into any Unity project that wants it, with or without the +kit. [Run tests from the command line](../how-to/running-tests-from-the-command-line) walks through +using it. + +## Getting it + +The script is `Scripts~/Invoke-UnityTests.ps1` in the +[kit's repository](https://github.com/tsvrc/udon-test-kit). A Creator Companion install also has +it on disk, under `Packages/com.tsvrc.udon-test-kit/Scripts~`. Unity doesn't import folders ending +in `~`, so a `.unitypackage` of the kit can't include it. + +Copy it anywhere inside your project, such as its root folder. It finds the project by looking up +from its own folder. + +## Running it + +Close the Unity Editor first: batch mode can't open a project that another Editor has open. Then: + +```powershell +.\Invoke-UnityTests.ps1 +``` + +`Get-Help .\Invoke-UnityTests.ps1 -Detailed` shows the same options as the table below. + +## Options + +| Option | Default | +| --- | --- | +| `-TestMode` | `All`, which runs `EditMode` and then `PlayMode`. Pass one of those to run one mode. | +| `-AssemblyNames` | Your project's own assemblies, separated with `;` (see below). | +| `-ProjectPath` | The nearest folder with a `ProjectSettings\ProjectVersion.txt`, looking up from the script's folder, then from the current folder. | +| `-UnityPath` | The Editor version in `ProjectVersion.txt`, in the Unity Hub default install folder under `Program Files`. | +| `-ResultsPath` | A new folder under the temp folder, named after the date and time. | + +Without assembly names, Unity runs every test it finds, including the tests that come with +packages, such as the VRChat SDK. The default list is every assembly definition under `Assets`, +plus the assemblies Unity compiles scripts without one into (`Assembly-CSharp`, +`Assembly-CSharp-firstpass`, `Assembly-CSharp-Editor` and `Assembly-CSharp-Editor-firstpass`), so +tests in `Editor` folders run too. A name with no tests in it is harmless. + +## What it does + +Each test mode starts Unity once, in batch mode, with `-runTests`. Unity writes the mode's results +to `.xml` and its log to `.log` in the results folder. + +For each mode, the script prints every test that didn't pass, with its result and message, then a +count: + +``` +PlayMode tests... +FAILED DoorTests.Open_ByNonOwner_StaysClosed + Expected: False + But was: True +12 passed, 1 failed, 0 inconclusive or skipped +``` + +An inconclusive test is an `Assume` that didn't hold, so it proved nothing. + +## Exit code + +The script exits with 1 when Unity reported a failed or inconclusive test, or a run that errored +out, and when a mode wrote no results, in which case it prints the log's path. Otherwise it exits +with 0, so a build server can use it as a step that fails the build. + +## Execution policy + +If PowerShell refuses to run the script because of its execution policy, allow local scripts once: + +```powershell +Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +``` + +It's been run with Windows PowerShell 5.1, which comes with Windows. diff --git a/docs/udon-test-kit/reference/private-field-access.md b/docs/udon-test-kit/reference/private-field-access.md new file mode 100644 index 0000000..b753a02 --- /dev/null +++ b/docs/udon-test-kit/reference/private-field-access.md @@ -0,0 +1,49 @@ +--- +id: private-field-access +title: PrivateFieldAccess +sidebar_position: 5 +--- + +# PrivateFieldAccess + +`Tsvrc.UdonTestKit.PrivateFieldAccess` reads and writes fields, and calls methods, by name on any object +or type, whatever their access level. Use it to arrange or check state that a behaviour's public +members don't expose, without making that state public just for a test. + +```csharp +int remaining = PrivateFieldAccess.GetField(timer, "_remainingSeconds"); +PrivateFieldAccess.SetField(timer, "_remainingSeconds", 1); +PrivateFieldAccess.InvokeInstance(timer, "Tick"); +``` + +It works in Edit Mode and Play Mode tests. + +## Members + +| Member | Does | +| --- | --- | +| `SetField(object target, string fieldName, object value)` | Sets an instance field. | +| `SetField(Type staticType, string fieldName, object value)` | Sets a static field. | +| `GetField(object target, string fieldName)` | Returns an instance field's value as `T`. | +| `GetField(Type staticType, string fieldName)` | Returns a static field's value as `T`. | +| `InvokeInstance(object target, string methodName, params object[] args)` | Calls an instance method and returns its result. | +| `InvokeStatic(Type staticType, string methodName, params object[] args)` | Calls a static method and returns its result. | + +For a static class, pass `typeof(MyStaticClass)` as the type. + +## Lookup + +Each call looks for the member on the target's type, then on each base class in turn, and takes +the first match, public or not. `Type.GetField` and `Type.GetMethod` alone don't find a private +member declared on a base class, which is the usual case for state a framework base class keeps. + +## Failure modes + +- A name that isn't found on the type or any base class fails the test with a message such as + `Field '_remaining' not found on Timer.` A renamed member shows up as a failing test, not as a + silent no-op. +- Methods are found by name alone. A type that declares several overloads of the method throws + `AmbiguousMatchException`. +- `GetField` casts the value to `T`, so a wrong `T` throws `InvalidCastException`. +- An exception thrown by a method called through `InvokeInstance` or `InvokeStatic` reaches the + test wrapped in a `TargetInvocationException`; its `InnerException` is the original. diff --git a/docs/udon-test-kit/reference/test-assembly-creator.md b/docs/udon-test-kit/reference/test-assembly-creator.md new file mode 100644 index 0000000..0207d92 --- /dev/null +++ b/docs/udon-test-kit/reference/test-assembly-creator.md @@ -0,0 +1,57 @@ +--- +id: test-assembly-creator +title: TestAssemblyCreator +sidebar_position: 8 +--- + +# TestAssemblyCreator + +`Tsvrc.UdonTestKit.TestAssemblyCreator` creates the test assemblies a world needs: an Edit Mode test +assembly, a Play Mode test assembly, and a test doubles assembly both of them reference. Run it +from **Assets > Create > TsVRC > Udon Test Kit > Test Assemblies**, or from the Project window's +right-click **Create** menu. + +## Where it creates them + +It creates a `Tests` folder inside the folder selected in the Project window, or inside the folder +of a selected file. When nothing under `Assets` is selected, it uses `Assets`. An existing `Tests` +folder is left alone, and the new one is named `Tests 1`, `Tests 2` and so on. + +## What it creates + +The assembly names start with your project's **Product Name** from Player Settings, without spaces +or symbols. A name that ends up empty, or starts with a digit, becomes `World`. For a product named +"My World": + +| Folder | Assembly | Platforms | Example | +| --- | --- | --- | --- | +| `Tests/EditMode` | `MyWorld.Tests.EditMode` | Editor | `ExampleEditModeTests.cs` | +| `Tests/PlayMode` | `MyWorld.Tests.PlayMode` | every platform | `ExamplePlayModeTests.cs` | +| `Tests/Doubles` | `MyWorld.Tests.Doubles` | every platform | `ExampleSpy.cs` | + +When assemblies with those names already exist, the new ones get a number, such as +`MyWorld2.Tests.EditMode`. Two assemblies with the same name stop a project compiling. + +Each assembly references the kit, UdonSharp, the VRChat SDK, ClientSim, and every assembly +definition of yours with scripts under `Assets`, other than test assemblies. The two test assemblies +also reference the doubles assembly and Unity Test Framework. All three compile only when Unity +Test Framework includes tests, so none of them reaches a world build, and none is referenced +automatically by other assemblies. + +The `Doubles` assembly is for behaviours your tests add to GameObjects, such as +[`CallbackSpy`](./callback-spy-and-call-log) subclasses. It has no NUnit reference. Unity won't add +a behaviour from the Editor-only Edit Mode assembly to a GameObject, and Unity requires Edit Mode +and Play Mode tests to be in separate assemblies, so behaviours both kinds of test use go here. + +Each example compiles and passes as created. + +## Edge cases + +- The list of your assemblies is read once, when you create the test assemblies. Add an assembly + definition later, and add it to the test assemblies references yourself. +- Scripts without an assembly definition compile into `Assembly-CSharp`, which an assembly + definition can't reference. When any script under `Assets` has none, creating the assemblies logs + a warning. See + [Test scripts that have no assembly definition](../how-to/testing-scripts-without-an-assembly-definition). +- `TestAssemblyCreator.Create(string parentFolder = "Assets")` does the same from code, and returns + the new folder's path. `TestAssemblyCreator.MenuPath` is the menu item's path. diff --git a/docs/udon-test-kit/reference/test-run.md b/docs/udon-test-kit/reference/test-run.md new file mode 100644 index 0000000..0dcb253 --- /dev/null +++ b/docs/udon-test-kit/reference/test-run.md @@ -0,0 +1,44 @@ +--- +id: test-run +title: TestRun +sidebar_position: 6 +--- + +# TestRun + +`Tsvrc.UdonTestKit.TestRun.IsActive` tells editor tooling whether a Unity Test Framework run is in +progress. Tooling that rewrites assets when the domain reloads, an asset is imported or a scene +changes, such as a code generator, can check it and leave a test run alone. + +```csharp +if (TestRun.IsActive || EditorApplication.isPlayingOrWillChangePlaymode) +{ + return; +} +``` + +## When it's true + +`IsActive` is true: + +- from the moment a run reports its start until it reports its end or an error, whether it was + started from the Test Runner window or from the command line, and +- for the whole life of a batch-mode Unity process started with `-runTests`, from before the first + test is found. + +The start and end come from the public Unity Test Framework callbacks, which the kit registers again +on every domain load, since registered callbacks don't survive a reload. The flag between them +lives in `SessionState`, so it survives the domain reloads a run causes but not an Editor restart. +A run that crashes without reporting its end can't leave it set for good. + +## The reload into Play Mode + +A Play Mode run reports its start only after the domain reload into Play Mode. Tooling that reacts +to that reload sees `IsActive` false, so check `EditorApplication.isPlayingOrWillChangePlaymode` +as well, as in the example above. + +## Using it + +`TestRun` uses Editor APIs, so call it from Editor code. It's in the kit's `Tsvrc.UdonTestKit` assembly: +scripts without an assembly definition can use it directly, and code in an assembly definition +needs a reference to `Tsvrc.UdonTestKit`. diff --git a/docs/udon-test-kit/reference/test-scene.md b/docs/udon-test-kit/reference/test-scene.md new file mode 100644 index 0000000..cd0d805 --- /dev/null +++ b/docs/udon-test-kit/reference/test-scene.md @@ -0,0 +1,82 @@ +--- +id: test-scene +title: TestScene +sidebar_position: 3 +--- + +# TestScene + +`Tsvrc.UdonTestKit.TestScene` is a scene for Play Mode tests to run in: a new empty scene, or a scene +asset such as your world's own. Loading it makes it the active scene, so ClientSim and the objects +a test creates go into it, and unloading it destroys them. + +[`ClientSimTestBase`](./clientsim-test-base) loads and unloads one for you when you pass it to the +base constructor, as in [Run tests in a scene of their own](../how-to/running-tests-in-their-own-scene). +You can also use it on its own. + +## Creating one + +- `TestScene.Empty(string name)` is a new empty scene with that name. It's created each time it's + loaded, so every load starts empty. +- `TestScene.FromAsset(string assetPath)` is the scene asset at that path, such as + `"Assets/Scenes/MyWorld.unity"`. It doesn't need to be in the build settings. + +Neither one loads anything until `Load` runs. + +## Members + +`Load()` is a coroutine: `yield return scene.Load()`. It loads the scene alongside the scenes +already loaded, so the Unity Test Framework test scene stays loaded, and makes it the active +scene. It does nothing when the scene is already loaded. + +`Unload()` makes the scene that was active before `Load` active again, then unloads this scene and +everything in it. It finishes before it returns, so it also works where a test can't wait, such as +in `[OneTimeTearDown]`. It does nothing when the scene isn't loaded. + +`IsLoaded` is true between a `Load` that finished and the next `Unload`. + +## Using it without the base class + +Load it in a `[UnitySetUp]` and unload it once ClientSim has ended, such as in a +`[UnityTearDown]`: + +```csharp +private readonly TestScene _scene = TestScene.Empty("Inventory"); +private readonly ClientSimSession _session = new ClientSimSession(); + +[UnitySetUp] +public IEnumerator SetUp() +{ + yield return _scene.Load(); +} + +[UnityTearDown] +public IEnumerator TearDown() +{ + _session.End(); + _scene.Unload(); + yield break; +} +``` + +End the session before unloading the scene, the same order `ClientSimTestBase` uses. + +## Edge cases + +- `TestScene` works only in Play Mode tests. `FromAsset` loads through + `EditorSceneManager.LoadSceneInPlayMode`, which is why the scene doesn't need to be in the build + settings. +- Unity drops the operation `LoadSceneInPlayMode` loads with. When the garbage collector finalized + it as Unity exited, Unity crashed after every test had passed, so `Load` finalizes it right after + the scene loads, while that's still safe. +- `Unload` uses the synchronous Unity `SceneManager.UnloadScene`, which Unity marks obsolete because + it's unsafe inside physics callbacks. A test's setup and teardown aren't physics callbacks. + +## TestSceneLifetime + +`Tsvrc.UdonTestKit.TestSceneLifetime` tells `ClientSimTestBase` how long its scene lives: + +- `PerTest`, the default, loads a fresh copy for every test and unloads it after. Each test starts + from the same scene, and whatever a test created is gone before the next one. +- `PerFixture` loads it for the first test in the class and unloads it after the last. The tests + share whatever the earlier ones changed in it. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index aa63bbb..866a7d9 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -6,7 +6,7 @@ import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { title: 'TsVRC', - tagline: 'A community-maintained framework for building VRChat worlds', + tagline: 'Open-source tools for building VRChat worlds', favicon: 'img/tsvrc-logo.png', // Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future @@ -28,7 +28,21 @@ const config: Config = { markdown: { mermaid: true, }, - themes: ['@docusaurus/theme-mermaid'], + themes: [ + '@docusaurus/theme-mermaid', + [ + // Builds the search index at build time, so search works on GitHub Pages with no + // external service. + '@easyops-cn/docusaurus-search-local', + { + hashed: true, + language: ['en', 'es'], + indexBlog: false, + explicitSearchResultPath: true, + highlightSearchTermsOnTargetPage: true, + }, + ], + ], i18n: { defaultLocale: 'en', @@ -69,11 +83,6 @@ const config: Config = { src: 'img/tsvrc-logo.png', }, items: [ - { - to: '/framework', - position: 'left', - label: 'Framework', - }, { type: 'docSidebar', sidebarId: 'tutorialSidebar', @@ -99,13 +108,9 @@ const config: Config = { footer: { style: 'dark', links: [ - { - label: 'Framework', - to: '/framework', - }, { label: 'Docs', - to: '/docs/tsvrc/intro', + to: '/docs/core/intro', }, { label: 'GitHub', diff --git a/i18n/es/code.json b/i18n/es/code.json index 70748de..6fe5273 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -1,39 +1,39 @@ { - "framework.notice.title": { + "core.notice.title": { "message": "Antes de construir con esto" }, - "framework.notice.vrchat": { + "core.notice.vrchat": { "message": "TsVRC no está afiliado a VRChat Inc. ni cuenta con su respaldo." }, - "framework.notice.body": { - "message": "Además, sigue siendo un trabajo en curso. Cada parte tiene sus propios tests, y los ejecutamos antes de cada versión, pero no podemos prometer haber cubierto todos los casos. Guarda una copia de seguridad de tu proyecto antes de añadir TsVRC o de actualizarlo, solo por si acaso. Si algo se rompe de tu lado, no podemos arreglarlo por ti, pero nos gustaría mucho saberlo. Abre un issue en GitHub y le echaremos un vistazo." + "core.notice.body": { + "message": "Además, sigue siendo un trabajo en curso. Cada parte tiene sus propios tests, y los ejecutamos antes de cada versión, pero no podemos prometer haber cubierto todos los casos. Guarda una copia de seguridad de tu proyecto antes de añadir Core o de actualizarlo, solo por si acaso. Si algo se rompe de tu lado, no podemos arreglarlo por ti, pero nos gustaría mucho saberlo. Abre un issue en GitHub y le echaremos un vistazo." }, - "homepage.hero.title": { - "message": "Herramientas para crear mundos y avatares de VRChat" + "homepage.hero.greeting": { + "message": "Hola, soy {name}" }, - "homepage.hero.subtitle": { - "message": "TsVRC crea herramientas de código abierto para creadores de VRChat, empezando con un framework para estructurar código de mundos en UdonSharp." + "homepage.hero.avatarAlt": { + "message": "Toni en GitHub" }, - "homepage.hero.cta.explore": { - "message": "Explorar TsVRC" + "homepage.hero.intro": { + "message": "Creo herramientas para crear mundos de VRChat. TsVRC es el nombre con el que las publico, y la mayoría son de código abierto." }, - "homepage.hero.cta.github": { - "message": "Ver en GitHub" + "homepage.projects.core.description": { + "message": "Una forma estructurada de crear mundos de VRChat con UdonSharp: inicio consistente, dependencias ya conectadas y código generado en vez de conectarlo todo a mano." }, - "homepage.trust.starsAlt": { - "message": "Estrellas de GitHub de tsvrc/tsvrc-core" + "homepage.projects.udonTestKit.description": { + "message": "Tests automatizados para mundos de VRChat: los tests de modo Play ejecutan tus behaviours de UdonSharp en una sesión real de ClientSim, con jugadores reales." }, - "homepage.trust.lastCommitAlt": { - "message": "Último commit de tsvrc/tsvrc-core" + "homepage.projects.github": { + "message": "{name} en GitHub" }, - "homepage.trust.licenseAlt": { - "message": "Licencia de tsvrc/tsvrc-core" + "homepage.projects.stat.stars": { + "message": "Estrellas" }, - "homepage.projects.tsvrc.description": { - "message": "Una forma estructurada de crear mundos de VRChat con UdonSharp: inicio consistente, dependencias ya conectadas y código generado en vez de conectarlo todo a mano." + "homepage.projects.stat.license": { + "message": "Licencia" }, - "homepage.projects.learnMore": { - "message": "Más información" + "homepage.projects.stat.release": { + "message": "Última versión" }, "homepage.projects.status.available": { "message": "Disponible" @@ -45,135 +45,135 @@ "message": "Proyectos" }, "homepage.projects.description": { - "message": "Cada proyecto de TsVRC vive en este mismo sitio. Por ahora solo hay uno; más llegarán aquí a medida que se publiquen." + "message": "Todo lo construido hasta ahora. Cada proyecto tiene su documentación aquí y su código en GitHub." }, "homepage.meta.title": { "message": "TsVRC" }, "homepage.meta.description": { - "message": "TsVRC crea herramientas de código abierto para creadores de mundos y avatares de VRChat." + "message": "Herramientas de código abierto de Toni para crear mundos de VRChat, publicadas como TsVRC." }, - "framework.hero.title": { + "core.hero.title": { "message": "Un framework para mundos de VRChat construido sobre UdonSharp" }, - "framework.hero.subtitle": { + "core.hero.subtitle": { "message": "Inicialización estructurada, conexión de dependencias y generación de código en el editor, para que tus scripts de mundo hereden de una clase base tipada en vez de conectar referencias a mano cada vez que agregas un behaviour." }, - "framework.hero.cta.docs": { + "core.hero.cta.docs": { "message": "Leer la documentación" }, - "framework.hero.cta.github": { + "core.hero.cta.github": { "message": "Ver en GitHub" }, - "framework.hero.jumpToLabel": { + "core.hero.jumpToLabel": { "message": "Ir a:" }, - "framework.hero.jump.why": { + "core.hero.jump.why": { "message": "Por qué existe" }, - "framework.hero.jump.blocks": { + "core.hero.jump.blocks": { "message": "Las piezas fundamentales" }, - "framework.hero.jump.start": { + "core.hero.jump.start": { "message": "Empezar" }, - "framework.hero.scrollCue": { + "core.hero.scrollCue": { "message": "Desplázate para saber más" }, - "framework.hero.annotation.behaviour.title": { + "core.hero.annotation.behaviour.title": { "message": "TsBehaviour" }, - "framework.hero.annotation.behaviour.description": { + "core.hero.annotation.behaviour.description": { "message": "La clase generada de la que hereda cada script. Es lo que le da a este behaviour su referencia _ts." }, - "framework.hero.annotation.globals.title": { + "core.hero.annotation.globals.title": { "message": "Globals" }, - "framework.hero.annotation.globals.description": { + "core.hero.annotation.globals.description": { "message": "Cualquier objeto de la escena con nombre, accesible como _ts.Name desde cualquier behaviour, sin arrastrar referencias a mano." }, - "framework.hero.annotation.memory.title": { + "core.hero.annotation.memory.title": { "message": "Memory" }, - "framework.hero.annotation.memory.description": { + "core.hero.annotation.memory.description": { "message": "El almacén de clave-valor compartido, accesible como _ts.Memory desde cualquier parte." }, - "framework.hero.annotation.more.title": { + "core.hero.annotation.more.title": { "message": "Y eso no es todo" }, - "framework.hero.annotation.more.description": { - "message": "Y hay más de donde vino eso. Mira el resto de las piezas fundamentales de TsVRC más abajo." + "core.hero.annotation.more.description": { + "message": "Y hay más de donde vino eso. Mira el resto de las piezas fundamentales de Core más abajo." }, - "framework.why.title": { + "core.why.title": { "message": "Por qué existe" }, - "framework.why.paragraph1": { + "core.why.paragraph1": { "message": "Cada script de UdonSharp se inicia por su cuenta, sin garantía de en qué orden pasan las cosas y sin una forma fácil de llegar a otro script en tu mundo. La mayoría de los creadores lo resuelve a mano: arrastrando referencias entre objetos en el Inspector, y con la esperanza de que todo siga conectado cuando el mundo carga." }, - "framework.why.paragraph2": { - "message": "TsVRC le da a cada script una única referencia compartida, {code}, para llegar a cualquier otra cosa de tu proyecto. La configuración ocurre en un orden que tú controlas, y TsVRC descubre de antemano cuál de tus propias clases usar, en el editor de Unity. Consulta {link} para ver el panorama completo." + "core.why.paragraph2": { + "message": "Core le da a cada script una única referencia compartida, {code}, para llegar a cualquier otra cosa de tu proyecto. La configuración ocurre en un orden que tú controlas, y Core descubre de antemano cuál de tus propias clases usar, en el editor de Unity. Consulta {link} para ver el panorama completo." }, - "framework.why.linkText": { - "message": "cómo encaja TsVRC" + "core.why.linkText": { + "message": "cómo encaja Core" }, - "framework.features.constructs.title": { + "core.features.constructs.title": { "message": "Constructs" }, - "framework.features.constructs.description": { - "message": "Registra un TsvrcBehaviour una vez y TsVRC lo inicializa por ti, en un orden fijo, mediante TsStart, en vez de que tengas que encargarte tú de iniciarlo a mano." + "core.features.constructs.description": { + "message": "Registra un TsvrcBehaviour una vez y Core lo inicializa por ti, en un orden fijo, mediante TsStart, en vez de que tengas que encargarte tú de iniciarlo a mano." }, - "framework.features.globals.title": { + "core.features.globals.title": { "message": "Globals" }, - "framework.features.globals.description": { + "core.features.globals.description": { "message": "Señala un objeto de la escena una vez y cualquier behaviour puede leerlo de vuelta por su nombre, en vez de arrastrar la misma referencia a una docena de campos del Inspector." }, - "framework.features.memory.title": { + "core.features.memory.title": { "message": "Memory" }, - "framework.features.memory.description": { + "core.features.memory.description": { "message": "Un almacén de clave-valor compartido con niveles efímero, persistente y sincronizado, para que llevar un valor a todos los jugadores no signifique escribir tu propia capa de red." }, - "framework.features.factories.title": { + "core.features.factories.title": { "message": "Factories" }, - "framework.features.factories.description": { + "core.features.factories.description": { "message": "Instancia un prefab bajo demanda con una llamada _ts.Create... generada, para cualquier cosa que no necesite mantenerse sincronizada entre jugadores." }, - "framework.features.pools.title": { + "core.features.pools.title": { "message": "Pools" }, - "framework.features.pools.description": { + "core.features.pools.description": { "message": "Pre-instancia y conecta exactamente las instancias que un prefab en red necesita, en tiempo de edición, porque VRChat no puede sincronizar un objeto creado mientras el mundo está en ejecución." }, - "framework.features.instance.title": { + "core.features.instance.title": { "message": "Instance" }, - "framework.features.instance.description": { + "core.features.instance.description": { "message": "Un behaviour por proyecto que representa la instancia de mundo en ejecución, encontrado y conectado automáticamente, para que comprobaciones como \"¿soy el master?\" tengan un único lugar obvio." }, - "framework.features.title": { + "core.features.title": { "message": "Las piezas fundamentales" }, - "framework.features.subtitle": { + "core.features.subtitle": { "message": "Seis piezas generadas cubren la mayor parte de lo que necesita un mundo. Registra o señala algo una vez, y luego alcánzalo de la misma forma desde cualquier behaviour." }, - "framework.features.detailsLink": { + "core.features.detailsLink": { "message": "Detalles →" }, - "framework.cta.title": { + "core.cta.title": { "message": "Empieza a construir" }, - "framework.cta.subtitle": { - "message": "Agrega TsVRC mediante VCC y luego sigue la guía de introducción para tener un behaviour funcionando en Play Mode." + "core.cta.subtitle": { + "message": "Agrega Core mediante VCC y luego sigue la guía de introducción para tener un behaviour funcionando en Play Mode." }, - "framework.cta.button": { + "core.cta.button": { "message": "Empezar" }, - "framework.meta.title": { + "core.meta.title": { "message": "TsVRC Core" }, - "framework.meta.description": { + "core.meta.description": { "message": "TsVRC Core es un framework para crear mundos de VRChat con UdonSharp: inicialización estructurada, conexión de dependencias y generación de código en el editor." }, "support.meta.title": { @@ -202,5 +202,35 @@ }, "support.options.kofi.cta": { "message": "Apoya en Ko-fi" + }, + "theme.SearchBar.label": { + "message": "Buscar" + }, + "theme.SearchBar.seeAll": { + "message": "Ver todos los resultados" + }, + "theme.SearchBar.noResultsText": { + "message": "Sin resultados" + }, + "theme.SearchBar.searchInContext": { + "message": "Ver todos los resultados en \"{context}\"" + }, + "theme.SearchBar.seeAllOutsideContext": { + "message": "Ver todos los resultados fuera de \"{context}\"" + }, + "theme.SearchPage.existingResultsTitle": { + "message": "Resultados de búsqueda para \"{query}\"" + }, + "theme.SearchPage.emptyResultsTitle": { + "message": "Buscar en la documentación" + }, + "theme.SearchPage.documentsFound.plurals": { + "message": "1 documento encontrado|{count} documentos encontrados" + }, + "theme.SearchPage.noResultsText": { + "message": "No se encontraron documentos" + }, + "theme.SearchPage.searchContext.everywhere": { + "message": "En todas partes" } } diff --git a/i18n/es/docusaurus-plugin-content-docs/current/core/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/core/intro.md new file mode 100644 index 0000000..28983dd --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/core/intro.md @@ -0,0 +1,46 @@ +--- +id: intro +title: TsVRC Core +sidebar_position: 1 +--- + +# TsVRC Core + +TsVRC Core es un framework de código abierto para crear mundos de +VRChat con UdonSharp: inicialización estructurada, conexión de dependencias y +generación de código en el editor (codegen) que escribe ese código de conexión por ti, +sobre el VRChat Worlds SDK. + +:::warning[Experimental y pre-1.0] +Core es un experimento, y herramientas de programación con IA generaron parte de sus funciones. No +está hecho por completo con vibe coding, pero la IA tuvo una gran participación al escribirlo. +Además, todavía es pre-1.0: todo lo documentado aquí, incluyendo el comportamiento de casos límite +en las páginas de referencia, describe lo que el código hace hoy, no un contrato de API cerrado. + +Prueba el comportamiento del que depende tu mundo antes de publicarlo. Si buscas proyectos listos +para usar, mira [Udon Test Kit](/docs/udon-test-kit/intro) y +[Udon Tools Kit](https://github.com/tsvrc/udon-tools-kit). +::: + +## ¿Eres nuevo en Core? + +Empieza por [Añade TsVRC Core a tu proyecto](./add-to-your-project) y luego +[Construye tu primer behaviour](./first-behaviour): al final tendrás un script +funcionando en modo Play. + +## ¿Ya lo usas? + +Ve directo a lo que necesitas: + +- **Resolver una tarea concreta** — + [agrupar un prefab en un pool](./how-to/pooling-a-prefab), + [mostrar las posiciones de los jugadores](./how-to/showing-player-positions), + [transferir datos entre clientes](./how-to/transferring-data-between-clients) o + [conectar un ready check](./how-to/wiring-a-ready-check). +- **Buscar el comportamiento de un tipo concreto** — busca su nombre, empieza por + [Cómo encaja Core](./core-concepts/how-it-fits-together) y sigue sus enlaces, o + explora Reference en la barra lateral, agrupado por área: jugadores, red, UI, codegen, + etc. +- **Entender por qué Core funciona como funciona** — + [Codegen en vez de reflexión](./explanations/codegen-vs-reflection) y el resto de + páginas en Explanations. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md b/i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md new file mode 100644 index 0000000..b81cce9 --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/install-a-package.md @@ -0,0 +1,41 @@ +--- +id: install-a-package +title: Instalar un paquete de TsVRC +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Instalar un paquete de TsVRC + +Todos los paquetes de TsVRC se instalan de la misma forma. La página de instalación de cada +proyecto indica qué paquete añadir, qué necesita y cómo comprobar que funcionó. + + + + +1. Añade el repositorio de TsVRC al Creator Companion, una vez por computadora: haz clic en + [Add to VCC](vcc://vpm/addRepo?url=https%3A%2F%2Fvpm.tsvrc.com%2Findex.json), o añade + `https://vpm.tsvrc.com/index.json` como se describe en la guía + [Community Repositories](https://vcc.docs.vrchat.com/guides/community-repositories/) de VRChat. +2. Haz clic en **Manage Project** junto a tu proyecto y luego en **+** junto al paquete. + +El Creator Companion añade el paquete a la carpeta `Packages` de tu proyecto, junto con los +paquetes de los que depende, y resuelve la versión del VRChat Worlds SDK que necesita. + + + + +1. Descarga el `.unitypackage` más reciente del paquete desde la página de releases de GitHub que + enlaza su página de instalación. +2. Con tu proyecto abierto, haz doble clic en el archivo, o usa **Assets > Import Package > Custom + Package**, e importa todo. + +El paquete queda en la carpeta `Packages` de tu proyecto. Un `.unitypackage` no puede instalar los +paquetes de los que depende ni comprobar tu versión del VRChat Worlds SDK, así que importa esos +paquetes y comprueba la versión tú mismo. En un proyecto que usa el Creator Companion para otros +paquetes, úsalo también para este: las actualizaciones y dependencias de un paquete importado a +mano quedan a tu cargo. + + + diff --git a/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md deleted file mode 100644 index 378feeb..0000000 --- a/i18n/es/docusaurus-plugin-content-docs/current/tsvrc/intro.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: intro -title: TsVRC -sidebar_position: 1 ---- - -# TsVRC - -TsVRC es un framework de código abierto, mantenido por la comunidad, para crear mundos de -VRChat con UdonSharp: inicialización estructurada, conexión de dependencias y -generación de código en el editor (codegen) que escribe ese código de conexión por ti, -sobre el VRChat Worlds SDK. - -:::note -TsVRC todavía es pre-1.0. Todo lo documentado aquí, incluyendo el comportamiento de -casos límite en las páginas de referencia, describe lo que el código hace hoy, no un -contrato de API cerrado. -::: - -## ¿Eres nuevo en TsVRC? - -Empieza por [Añade TsVRC a tu proyecto](./add-to-your-project) y luego -[Construye tu primer behaviour](./first-behaviour): al final tendrás un script -funcionando en modo Play. - -## ¿Ya lo usas? - -Ve directo a lo que necesitas: - -- **Resolver una tarea concreta** — - [agrupar un prefab en un pool](./how-to/pooling-a-prefab), - [mostrar las posiciones de los jugadores](./how-to/showing-player-positions), - [transferir datos entre clientes](./how-to/transferring-data-between-clients) o - [conectar un ready check](./how-to/wiring-a-ready-check). -- **Buscar el comportamiento de un tipo concreto** — empieza por - [Cómo encaja TsVRC](./core-concepts/how-it-fits-together) y sigue sus enlaces, o - explora Reference en la barra lateral, agrupado por área: jugadores, red, UI, codegen, - etc. -- **Entender por qué TsVRC funciona como funciona** — - [Codegen en vez de reflexión](./explanations/codegen-vs-reflection) y el resto de - páginas en Explanations. diff --git a/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md new file mode 100644 index 0000000..c58785c --- /dev/null +++ b/i18n/es/docusaurus-plugin-content-docs/current/udon-test-kit/intro.md @@ -0,0 +1,60 @@ +--- +id: intro +title: Udon Test Kit +sidebar_position: 1 +--- + +# Udon Test Kit + +Udon Test Kit te permite escribir tests automatizados para un mundo de VRChat con Unity Test +Framework. Los tests de modo Play ejecutan tus behaviours de UdonSharp en una sesión real de +ClientSim, con jugadores reales. + +## Por qué probar tu mundo + +Un test automatizado es un pequeño fragmento de código que ejecuta una parte de tu mundo y +comprueba el resultado, por ejemplo, que una puerta solo se abra para el jugador que es su dueño. +Unity ejecuta estos tests con su +[Unity Test Framework](https://docs.unity3d.com/Packages/com.unity.test-framework@1.1/manual/index.html), +en el editor (modo Edit) o con el juego en marcha (modo Play). + +Una vez escritos, los tests se vuelven a ejecutar cuando quieras, todos de una vez. Un cambio que +rompe algo se nota enseguida, antes de subir el mundo, y puedes cambiar tu código sin miedo a +romper algo que no estás mirando. Esto importa sobre todo en lo que es más difícil de comprobar +jugando solo: varios jugadores, quién es dueño de qué y datos que tienen que sobrevivir cuando +alguien vuelve a entrar. Los +[consejos de pruebas y control de calidad](https://unity.com/es/how-to/testing-and-quality-assurance-tips-unity-projects) +de Unity lo explican con más detalle. + +## Qué puedes hacer con él + +- Crear y quitar jugadores, y elegir quién es el master. +- Darle un dueño a cualquier GameObject, para probar lo que ve un jugador que no es su dueño. +- Conservar los datos guardados entre sesiones, como un jugador que sale y vuelve a entrar. +- Registrar las llamadas que tu código hace por nombre, como con `SendCustomEvent`. +- Ejecutar cada test en una escena propia, vacía o la de tu mundo. +- Crear desde un elemento de menú los assemblies de test que necesita tu mundo. + +El kit solo se compila para los tests, así que nada de él llega a la build de tu mundo. + +También incluye `Invoke-UnityTests.ps1`, un script de PowerShell independiente que ejecuta los +tests de un proyecto con un solo comando y muestra solo lo que falló. Funciona en cualquier +proyecto de Unity, con o sin el kit. + +## Empieza aquí + +1. [Añade Udon Test Kit a tu proyecto](./add-to-your-project) +2. [Escribe tu primer test](./first-test) + +## Tareas comunes + +- [Ejecutar tests en una escena propia](./how-to/running-tests-in-their-own-scene) +- [Probar qué pasa con un jugador que no es el dueño](./how-to/testing-a-player-who-isnt-the-owner) +- [Comprobar que los datos guardados sobreviven al volver a entrar](./how-to/testing-saved-data) +- [Ejecutar tests desde la línea de comandos](./how-to/running-tests-from-the-command-line) + +## Referencia y contexto + +- [ClientSimSession](./reference/clientsim-session), donde empieza la mayoría de los tests +- [Lo que el kit resuelve](./explanations/what-the-kit-works-around) +- [Nota de decisión: una sesión nueva para cada test](./explanations/a-fresh-session-for-every-test) diff --git a/i18n/es/docusaurus-theme-classic/footer.json b/i18n/es/docusaurus-theme-classic/footer.json index 4a53372..a124e50 100644 --- a/i18n/es/docusaurus-theme-classic/footer.json +++ b/i18n/es/docusaurus-theme-classic/footer.json @@ -1,11 +1,7 @@ { - "link.item.label.Framework": { - "message": "Framework", - "description": "The label of footer link with label=Framework linking to /framework" - }, "link.item.label.Docs": { "message": "Documentación", - "description": "The label of footer link with label=Docs linking to /docs/tsvrc/intro" + "description": "The label of footer link with label=Docs linking to /docs/core/intro" }, "link.item.label.GitHub": { "message": "GitHub", diff --git a/i18n/es/docusaurus-theme-classic/navbar.json b/i18n/es/docusaurus-theme-classic/navbar.json index b6ae456..7eea6d3 100644 --- a/i18n/es/docusaurus-theme-classic/navbar.json +++ b/i18n/es/docusaurus-theme-classic/navbar.json @@ -7,10 +7,6 @@ "message": "Logotipo de TsVRC", "description": "The alt text of navbar logo" }, - "item.label.Framework": { - "message": "Framework", - "description": "Navbar item with label Framework" - }, "item.label.Docs": { "message": "Documentación", "description": "Navbar item with label Docs" diff --git a/package-lock.json b/package-lock.json index d9842e6..b9d75a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,15 +7,18 @@ "": { "name": "tsvrc-docs", "version": "0.0.0", + "license": "CC-BY-4.0", "dependencies": { "@docusaurus/core": "3.10.2", "@docusaurus/faster": "3.10.2", "@docusaurus/preset-classic": "3.10.2", "@docusaurus/theme-mermaid": "^3.10.2", + "@easyops-cn/docusaurus-search-local": "0.55.3", "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/space-grotesk": "^5.3.0", "@mdx-js/react": "^3.0.0", + "@primer/octicons-react": "19.40.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", @@ -4254,6 +4257,125 @@ "node": ">=20.0" } }, + "node_modules/@easyops-cn/autocomplete.js": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@easyops-cn/autocomplete.js/-/autocomplete.js-0.38.1.tgz", + "integrity": "sha512-drg76jS6syilOUmVNkyo1c7ZEBPcPuK+aJA7AksM5ZIIbV57DMHCywiCr+uHyv8BE5jUTU98j/H7gVrkHrWW3Q==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "immediate": "^3.2.3" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local": { + "version": "0.55.3", + "resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.55.3.tgz", + "integrity": "sha512-PlMKmxuonvZ1C+/zJS1hJaF1xaxD1TsUvOMzpP6DdQMtZqmTaYjcLGM12ePzl0m1rp3AgfTBeDbFNHQhwnH/dA==", + "license": "MIT", + "dependencies": { + "@docusaurus/plugin-content-docs": "^2 || ^3", + "@docusaurus/theme-translations": "^2 || ^3", + "@docusaurus/utils": "^2 || ^3", + "@docusaurus/utils-common": "^2 || ^3", + "@docusaurus/utils-validation": "^2 || ^3", + "@easyops-cn/autocomplete.js": "^0.38.1", + "@node-rs/jieba": "^1.6.0", + "cheerio": "^1.0.0", + "clsx": "^2.1.1", + "comlink": "^4.4.2", + "debug": "^4.2.0", + "fs-extra": "^10.0.0", + "klaw-sync": "^6.0.0", + "lunr": "^2.3.9", + "lunr-languages": "^1.4.0", + "mark.js": "^8.11.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@docusaurus/theme-common": "^2 || ^3", + "open-ask-ai": "^0.7.3", + "react": "^16.14.0 || ^17 || ^18 || ^19", + "react-dom": "^16.14.0 || 17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "open-ask-ai": { + "optional": true + } + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@easyops-cn/docusaurus-search-local/node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, "node_modules/@emnapi/core": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", @@ -4987,6 +5109,283 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@node-rs/jieba": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba/-/jieba-1.10.4.tgz", + "integrity": "sha512-GvDgi8MnBiyWd6tksojej8anIx18244NmIOc1ovEw8WKNUejcccLfyu8vj66LWSuoZuKILVtNsOy4jvg3aoxIw==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@node-rs/jieba-android-arm-eabi": "1.10.4", + "@node-rs/jieba-android-arm64": "1.10.4", + "@node-rs/jieba-darwin-arm64": "1.10.4", + "@node-rs/jieba-darwin-x64": "1.10.4", + "@node-rs/jieba-freebsd-x64": "1.10.4", + "@node-rs/jieba-linux-arm-gnueabihf": "1.10.4", + "@node-rs/jieba-linux-arm64-gnu": "1.10.4", + "@node-rs/jieba-linux-arm64-musl": "1.10.4", + "@node-rs/jieba-linux-x64-gnu": "1.10.4", + "@node-rs/jieba-linux-x64-musl": "1.10.4", + "@node-rs/jieba-wasm32-wasi": "1.10.4", + "@node-rs/jieba-win32-arm64-msvc": "1.10.4", + "@node-rs/jieba-win32-ia32-msvc": "1.10.4", + "@node-rs/jieba-win32-x64-msvc": "1.10.4" + } + }, + "node_modules/@node-rs/jieba-android-arm-eabi": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.10.4.tgz", + "integrity": "sha512-MhyvW5N3Fwcp385d0rxbCWH42kqDBatQTyP8XbnYbju2+0BO/eTeCCLYj7Agws4pwxn2LtdldXRSKavT7WdzNA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-android-arm64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-android-arm64/-/jieba-android-arm64-1.10.4.tgz", + "integrity": "sha512-XyDwq5+rQ+Tk55A+FGi6PtJbzf974oqnpyCcCPzwU3QVXJCa2Rr4Lci+fx8oOpU4plT3GuD+chXMYLsXipMgJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-darwin-arm64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-arm64/-/jieba-darwin-arm64-1.10.4.tgz", + "integrity": "sha512-G++RYEJ2jo0rxF9626KUy90wp06TRUjAsvY/BrIzEOX/ingQYV/HjwQzNPRR1P1o32a6/U8RGo7zEBhfdybL6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-darwin-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-darwin-x64/-/jieba-darwin-x64-1.10.4.tgz", + "integrity": "sha512-MmDNeOb2TXIZCPyWCi2upQnZpPjAxw5ZGEj6R8kNsPXVFALHIKMa6ZZ15LCOkSTsKXVC17j2t4h+hSuyYb6qfQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-freebsd-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-freebsd-x64/-/jieba-freebsd-x64-1.10.4.tgz", + "integrity": "sha512-/x7aVQ8nqUWhpXU92RZqd333cq639i/olNpd9Z5hdlyyV5/B65LLy+Je2B2bfs62PVVm5QXRpeBcZqaHelp/bg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm-gnueabihf": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm-gnueabihf/-/jieba-linux-arm-gnueabihf-1.10.4.tgz", + "integrity": "sha512-crd2M35oJBRLkoESs0O6QO3BBbhpv+tqXuKsqhIG94B1d02RVxtRIvSDwO33QurxqSdvN9IeSnVpHbDGkuXm3g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-gnu/-/jieba-linux-arm64-gnu-1.10.4.tgz", + "integrity": "sha512-omIzNX1psUzPcsdnUhGU6oHeOaTCuCjUgOA/v/DGkvWC1jLcnfXe4vdYbtXMh4XOCuIgS1UCcvZEc8vQLXFbXQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-arm64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-arm64-musl/-/jieba-linux-arm64-musl-1.10.4.tgz", + "integrity": "sha512-Y/tiJ1+HeS5nnmLbZOE+66LbsPOHZ/PUckAYVeLlQfpygLEpLYdlh0aPpS5uiaWMjAXYZYdFkpZHhxDmSLpwpw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-x64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-gnu/-/jieba-linux-x64-gnu-1.10.4.tgz", + "integrity": "sha512-WZO8ykRJpWGE9MHuZpy1lu3nJluPoeB+fIJJn5CWZ9YTVhNDWoCF4i/7nxz1ntulINYGQ8VVuCU9LD86Mek97g==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-linux-x64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-linux-x64-musl/-/jieba-linux-x64-musl-1.10.4.tgz", + "integrity": "sha512-uBBD4S1rGKcgCyAk6VCKatEVQb6EDD5I40v/DxODi5CuZVCANi9m5oee/MQbAoaX7RydA2f0OSCE9/tcwXEwUg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-wasm32-wasi": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-wasm32-wasi/-/jieba-wasm32-wasi-1.10.4.tgz", + "integrity": "sha512-Y2umiKHjuIJy0uulNDz9SDYHdfq5Hmy7jY5nORO99B4pySKkcrMjpeVrmWXJLIsEKLJwcCXHxz8tjwU5/uhz0A==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/jieba-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@node-rs/jieba-win32-arm64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-arm64-msvc/-/jieba-win32-arm64-msvc-1.10.4.tgz", + "integrity": "sha512-nwMtViFm4hjqhz1it/juQnxpXgqlGltCuWJ02bw70YUDMDlbyTy3grCJPpQQpueeETcALUnTxda8pZuVrLRcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-win32-ia32-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-ia32-msvc/-/jieba-win32-ia32-msvc-1.10.4.tgz", + "integrity": "sha512-DCAvLx7Z+W4z5oKS+7vUowAJr0uw9JBw8x1Y23Xs/xMA4Em+OOSiaF5/tCJqZUCJ8uC4QeImmgDFiBqGNwxlyA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/jieba-win32-x64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@node-rs/jieba-win32-x64-msvc/-/jieba-win32-x64-msvc-1.10.4.tgz", + "integrity": "sha512-+sqemSfS1jjb+Tt7InNbNzrRh1Ua3vProVvC4BZRPg010/leCbGFFiQHpzcPRfpxAXZrzG5Y0YBTsPzN/I4yHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5256,6 +5655,15 @@ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, + "node_modules/@primer/octicons-react": { + "version": "19.40.0", + "resolved": "https://registry.npmjs.org/@primer/octicons-react/-/octicons-react-19.40.0.tgz", + "integrity": "sha512-z1yyW2WaZxf9t+295e1Ajqj0KXWAg3ZaNk5Q7jMYYsz2e1Pzbya4bkpip+pd1ndmlaYh4kInlmCOAgyDFU+ncw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/@rspack/binding": { "version": "1.7.12", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.12.tgz", @@ -8749,6 +9157,12 @@ "node": ">=10" } }, + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", + "license": "Apache-2.0" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -10527,6 +10941,31 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -12136,6 +12575,12 @@ "node": ">=16.x" } }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -12781,6 +13226,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -13220,6 +13674,18 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, + "node_modules/lunr-languages": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/lunr-languages/-/lunr-languages-1.22.0.tgz", + "integrity": "sha512-zNog8491aJ5Fl4WGe+aiMcc851d7d5s31nuDjfR0qv5zoVFSBB8/nq/WLMP9Xrtx+UeLX7jDvKFEmus4V5aKZA==", + "license": "MPL-1.1" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -13230,6 +13696,12 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", @@ -16356,6 +16828,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -20271,6 +20755,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -21131,6 +21624,40 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9342498..c193102 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "@docusaurus/faster": "3.10.2", "@docusaurus/preset-classic": "3.10.2", "@docusaurus/theme-mermaid": "^3.10.2", + "@easyops-cn/docusaurus-search-local": "0.55.3", "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@fontsource/space-grotesk": "^5.3.0", "@mdx-js/react": "^3.0.0", + "@primer/octicons-react": "19.40.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", diff --git a/sidebars.ts b/sidebars.ts index 4aba22e..ca127ce 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -5,9 +5,9 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; /** * Each project under docs// gets one top-level category here, built from * autogenerated subtrees so individual pages never need a sidebar entry of their own. - * Adding a second project means adding its own top-level category the same way tsvrc's - * is built below, not relying on blanket autogeneration (see CLAUDE.md's Structure - * section for why). + * Adding another project means adding its own top-level category the same way core's + * and udon-test-kit's are built below, not relying on blanket autogeneration (see + * AGENTS.md's Gotchas section for why). * * Each labeled category below wraps a single-folder `autogenerated` subtree: pointing * `dirName` straight at a leaf folder returns its files as a flat list with no label of @@ -18,18 +18,18 @@ const sidebars: SidebarsConfig = { tutorialSidebar: [ { type: 'category', - label: 'Framework', - link: {type: 'doc', id: 'tsvrc/intro'}, + label: 'Core', + link: {type: 'doc', id: 'core/intro'}, items: [ { type: 'category', label: 'Getting started', - items: ['tsvrc/add-to-your-project', 'tsvrc/first-behaviour'], + items: ['core/add-to-your-project', 'core/first-behaviour'], }, { type: 'category', label: 'How-to guides', - items: [{type: 'autogenerated', dirName: 'tsvrc/how-to'}], + items: [{type: 'autogenerated', dirName: 'core/how-to'}], }, { type: 'category', @@ -38,24 +38,24 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Core concepts', - items: [{type: 'autogenerated', dirName: 'tsvrc/core-concepts'}], + items: [{type: 'autogenerated', dirName: 'core/core-concepts'}], }, { type: 'category', label: 'Players & tracking', - items: [{type: 'autogenerated', dirName: 'tsvrc/players-tracking'}], + items: [{type: 'autogenerated', dirName: 'core/players-tracking'}], }, { type: 'category', label: 'Networking & data', items: [ - 'tsvrc/networking-data/tsvrc-memory', - 'tsvrc/networking-data/tsvrc-timer', + 'core/networking-data/tsvrc-memory', + 'core/networking-data/tsvrc-timer', { type: 'category', label: 'Data transfer chain', - link: {type: 'doc', id: 'tsvrc/networking-data/data-transfer-overview'}, - items: [{type: 'autogenerated', dirName: 'tsvrc/networking-data/data-transfer'}], + link: {type: 'doc', id: 'core/networking-data/data-transfer-overview'}, + items: [{type: 'autogenerated', dirName: 'core/networking-data/data-transfer'}], }, ], }, @@ -66,25 +66,25 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'List', - items: [{type: 'autogenerated', dirName: 'tsvrc/ui-components/list'}], + items: [{type: 'autogenerated', dirName: 'core/ui-components/list'}], }, - 'tsvrc/ui-components/texture-graphics-2d', + 'core/ui-components/texture-graphics-2d', { type: 'category', label: 'Player position overlay', - items: [{type: 'autogenerated', dirName: 'tsvrc/ui-components/overlay'}], + items: [{type: 'autogenerated', dirName: 'core/ui-components/overlay'}], }, ], }, { type: 'category', label: 'Game flow', - items: [{type: 'autogenerated', dirName: 'tsvrc/game-flow'}], + items: [{type: 'autogenerated', dirName: 'core/game-flow'}], }, { type: 'category', label: 'Utilities', - items: [{type: 'autogenerated', dirName: 'tsvrc/utilities'}], + items: [{type: 'autogenerated', dirName: 'core/utilities'}], }, { type: 'category', @@ -93,34 +93,67 @@ const sidebars: SidebarsConfig = { { type: 'category', label: 'Config', - items: [{type: 'autogenerated', dirName: 'tsvrc/codegen/config'}], + items: [{type: 'autogenerated', dirName: 'core/codegen/config'}], }, { type: 'category', label: 'Modules', - items: [{type: 'autogenerated', dirName: 'tsvrc/codegen/modules'}], + items: [{type: 'autogenerated', dirName: 'core/codegen/modules'}], }, { type: 'category', label: 'Internals', - items: [{type: 'autogenerated', dirName: 'tsvrc/codegen/internals'}], + items: [{type: 'autogenerated', dirName: 'core/codegen/internals'}], }, ], }, { type: 'category', label: 'Testing', - items: [{type: 'autogenerated', dirName: 'tsvrc/testing'}], + items: [{type: 'autogenerated', dirName: 'core/testing'}], }, ], }, { type: 'category', label: 'Explanations', - items: [{type: 'autogenerated', dirName: 'tsvrc/explanations'}], + items: [{type: 'autogenerated', dirName: 'core/explanations'}], }, ], }, + { + type: 'category', + label: 'Udon Test Kit', + link: {type: 'doc', id: 'udon-test-kit/intro'}, + items: [ + { + type: 'category', + label: 'Getting started', + key: 'udon-test-kit-getting-started', + items: ['udon-test-kit/add-to-your-project', 'udon-test-kit/first-test'], + }, + { + type: 'category', + label: 'How-to guides', + key: 'udon-test-kit-how-to', + items: [{type: 'autogenerated', dirName: 'udon-test-kit/how-to'}], + }, + { + type: 'category', + label: 'Reference', + key: 'udon-test-kit-reference', + items: [{type: 'autogenerated', dirName: 'udon-test-kit/reference'}], + }, + { + type: 'category', + label: 'Explanations', + key: 'udon-test-kit-explanations', + items: [{type: 'autogenerated', dirName: 'udon-test-kit/explanations'}], + }, + ], + }, + // Shared by every project, so it sits at the docs root rather than under one project. + 'install-a-package', ], }; diff --git a/src/components/TsWindowMock/data.ts b/src/components/TsWindowMock/data.ts index b1f8cb8..f9401bf 100644 --- a/src/components/TsWindowMock/data.ts +++ b/src/components/TsWindowMock/data.ts @@ -69,7 +69,7 @@ export const TAB_CONFIG: Record = { "Register any scene object or component as a named field on _ts. After compiling, access it from any " + "TsvrcBehaviour via _ts.FieldName. Example: drag your GameManager here, then use _ts.GameManager from any " + "behaviour. Groups are organizational by default; toggle 'Namespace with group name' on a group to prefix " + - "its entries' member names (e.g. _ts.EnemiesSpawner).", + "its entries member names (e.g. _ts.EnemiesSpawner).", emptyHint: 'No globals registered yet. Add a scene object here to expose it as a field on TsGenerated.', showNames: true, entryIcon: 'script', diff --git a/src/components/framework/AnnotatedCode/AnnotationList.tsx b/src/components/core/AnnotatedCode/AnnotationList.tsx similarity index 97% rename from src/components/framework/AnnotatedCode/AnnotationList.tsx rename to src/components/core/AnnotatedCode/AnnotationList.tsx index 8744cc9..6aeb6b5 100644 --- a/src/components/framework/AnnotatedCode/AnnotationList.tsx +++ b/src/components/core/AnnotatedCode/AnnotationList.tsx @@ -1,6 +1,6 @@ import { usePrismTheme } from '@docusaurus/theme-common'; import useIsBrowser from '@docusaurus/useIsBrowser'; -import { colorClasses, type Annotation } from '@site/src/components/framework/AnnotatedCode/annotations'; +import { colorClasses, type Annotation } from '@site/src/components/core/AnnotatedCode/annotations'; export default function AnnotationList({ annotations }: { annotations: Annotation[] }) { const prismTheme = usePrismTheme(); diff --git a/src/components/framework/AnnotatedCode/Badge.tsx b/src/components/core/AnnotatedCode/Badge.tsx similarity index 89% rename from src/components/framework/AnnotatedCode/Badge.tsx rename to src/components/core/AnnotatedCode/Badge.tsx index 5a560f1..1f248c9 100644 --- a/src/components/framework/AnnotatedCode/Badge.tsx +++ b/src/components/core/AnnotatedCode/Badge.tsx @@ -1,4 +1,4 @@ -import { colorClasses, type AnnotationColor } from '@site/src/components/framework/AnnotatedCode/annotations'; +import { colorClasses, type AnnotationColor } from '@site/src/components/core/AnnotatedCode/annotations'; export default function Badge({ mark, color }: { mark: number; color: AnnotationColor }) { return ( diff --git a/src/components/framework/AnnotatedCode/CodePanel.tsx b/src/components/core/AnnotatedCode/CodePanel.tsx similarity index 97% rename from src/components/framework/AnnotatedCode/CodePanel.tsx rename to src/components/core/AnnotatedCode/CodePanel.tsx index 4d54577..c70360f 100644 --- a/src/components/framework/AnnotatedCode/CodePanel.tsx +++ b/src/components/core/AnnotatedCode/CodePanel.tsx @@ -2,13 +2,13 @@ import { Fragment } from 'react'; import { Highlight } from 'prism-react-renderer'; import { usePrismTheme } from '@docusaurus/theme-common'; import useIsBrowser from '@docusaurus/useIsBrowser'; -import Badge from '@site/src/components/framework/AnnotatedCode/Badge'; +import Badge from '@site/src/components/core/AnnotatedCode/Badge'; import { colorClasses, exampleCode, findTokenRun, type Annotation, -} from '@site/src/components/framework/AnnotatedCode/annotations'; +} from '@site/src/components/core/AnnotatedCode/annotations'; export default function CodePanel({ annotations }: { annotations: Annotation[] }) { const prismTheme = usePrismTheme(); diff --git a/src/components/framework/AnnotatedCode/annotations.ts b/src/components/core/AnnotatedCode/annotations.ts similarity index 81% rename from src/components/framework/AnnotatedCode/annotations.ts rename to src/components/core/AnnotatedCode/annotations.ts index fd70fd1..26bf8aa 100644 --- a/src/components/framework/AnnotatedCode/annotations.ts +++ b/src/components/core/AnnotatedCode/annotations.ts @@ -42,9 +42,9 @@ export function useAnnotations(): Annotation[] { line: 2, match: 'TsBehaviour', color: 'accent', - title: translate({ id: 'framework.hero.annotation.behaviour.title', message: 'TsBehaviour' }), + title: translate({ id: 'core.hero.annotation.behaviour.title', message: 'TsBehaviour' }), description: translate({ - id: 'framework.hero.annotation.behaviour.description', + id: 'core.hero.annotation.behaviour.description', message: "The generated shadow class every script extends. It's what gives this behaviour its _ts reference.", }), href: '#building-blocks', @@ -54,9 +54,9 @@ export function useAnnotations(): Annotation[] { line: 7, match: '_ts.Scoreboard', color: 'attention', - title: translate({ id: 'framework.hero.annotation.globals.title', message: 'Globals' }), + title: translate({ id: 'core.hero.annotation.globals.title', message: 'Globals' }), description: translate({ - id: 'framework.hero.annotation.globals.description', + id: 'core.hero.annotation.globals.description', message: 'Any named scene object, reachable as _ts.Name from every behaviour, no dragging references by hand.', }), href: '#building-blocks', @@ -66,9 +66,9 @@ export function useAnnotations(): Annotation[] { line: 8, match: '_ts.Memory', color: 'purple', - title: translate({ id: 'framework.hero.annotation.memory.title', message: 'Memory' }), + title: translate({ id: 'core.hero.annotation.memory.title', message: 'Memory' }), description: translate({ - id: 'framework.hero.annotation.memory.description', + id: 'core.hero.annotation.memory.description', message: 'The shared key-value store, reachable as _ts.Memory from anywhere.', }), href: '#building-blocks', @@ -76,10 +76,10 @@ export function useAnnotations(): Annotation[] { { mark: 4, color: 'success', - title: translate({ id: 'framework.hero.annotation.more.title', message: "That's not all" }), + title: translate({ id: 'core.hero.annotation.more.title', message: "That's not all" }), description: translate({ - id: 'framework.hero.annotation.more.description', - message: "There's more where that came from. See the rest of TsVRC's building blocks below.", + id: 'core.hero.annotation.more.description', + message: "There's more where that came from. See the rest of the Core building blocks below.", }), href: '#building-blocks', }, diff --git a/src/components/framework/AnnotatedCode/index.tsx b/src/components/core/AnnotatedCode/index.tsx similarity index 55% rename from src/components/framework/AnnotatedCode/index.tsx rename to src/components/core/AnnotatedCode/index.tsx index 987a583..c9a5592 100644 --- a/src/components/framework/AnnotatedCode/index.tsx +++ b/src/components/core/AnnotatedCode/index.tsx @@ -1,6 +1,6 @@ -import CodePanel from '@site/src/components/framework/AnnotatedCode/CodePanel'; -import AnnotationList from '@site/src/components/framework/AnnotatedCode/AnnotationList'; -import { useAnnotations } from '@site/src/components/framework/AnnotatedCode/annotations'; +import CodePanel from '@site/src/components/core/AnnotatedCode/CodePanel'; +import AnnotationList from '@site/src/components/core/AnnotatedCode/AnnotationList'; +import { useAnnotations } from '@site/src/components/core/AnnotatedCode/annotations'; export default function AnnotatedCode() { const annotations = useAnnotations(); diff --git a/src/components/framework/Features.tsx b/src/components/core/Features.tsx similarity index 71% rename from src/components/framework/Features.tsx rename to src/components/core/Features.tsx index 199f78c..36eb657 100644 --- a/src/components/framework/Features.tsx +++ b/src/components/core/Features.tsx @@ -13,80 +13,80 @@ function useFeatures(): Feature[] { { id: 'constructs', title: translate({ - id: 'framework.features.constructs.title', + id: 'core.features.constructs.title', message: 'Constructs', }), description: translate({ - id: 'framework.features.constructs.description', + id: 'core.features.constructs.description', message: - 'Register a TsvrcBehaviour once and TsVRC initializes it for you, in a fixed order, through TsStart, instead of you wiring startup by hand.', + 'Register a TsvrcBehaviour once and Core initializes it for you, in a fixed order, through TsStart, instead of you wiring startup by hand.', }), - href: '/docs/tsvrc/codegen/modules/construct-module', + href: '/docs/core/codegen/modules/construct-module', }, { id: 'globals', title: translate({ - id: 'framework.features.globals.title', + id: 'core.features.globals.title', message: 'Globals', }), description: translate({ - id: 'framework.features.globals.description', + id: 'core.features.globals.description', message: 'Point at a scene object once and every behaviour can read it back by name, instead of dragging the same reference into a dozen inspector slots.', }), - href: '/docs/tsvrc/codegen/modules/global-module', + href: '/docs/core/codegen/modules/global-module', }, { id: 'memory', title: translate({ - id: 'framework.features.memory.title', + id: 'core.features.memory.title', message: 'Memory', }), description: translate({ - id: 'framework.features.memory.description', + id: 'core.features.memory.description', message: "A shared key-value store with ephemeral, persistent, and synced tiers, so getting a value to every player doesn't mean writing your own networking.", }), - href: '/docs/tsvrc/networking-data/tsvrc-memory', + href: '/docs/core/networking-data/tsvrc-memory', }, { id: 'factories', title: translate({ - id: 'framework.features.factories.title', + id: 'core.features.factories.title', message: 'Factories', }), description: translate({ - id: 'framework.features.factories.description', + id: 'core.features.factories.description', message: "Spawn a prefab on demand with a generated _ts.Create... call, for anything that doesn't need to stay in sync across players.", }), - href: '/docs/tsvrc/codegen/modules/factory-module', + href: '/docs/core/codegen/modules/factory-module', }, { id: 'pools', title: translate({ - id: 'framework.features.pools.title', + id: 'core.features.pools.title', message: 'Pools', }), description: translate({ - id: 'framework.features.pools.description', + id: 'core.features.pools.description', message: "Pre-instantiate and wire exactly the instances a networked prefab needs, at edit time, because VRChat can't sync an object created while the world is running.", }), - href: '/docs/tsvrc/codegen/modules/pool-module', + href: '/docs/core/codegen/modules/pool-module', }, { id: 'instance', title: translate({ - id: 'framework.features.instance.title', + id: 'core.features.instance.title', message: 'Instance', }), description: translate({ - id: 'framework.features.instance.description', + id: 'core.features.instance.description', message: "One behaviour per project standing in for the running world instance itself, found and wired automatically, so instance-level checks like \"am I the master\" have one obvious home.", }), - href: '/docs/tsvrc/core-concepts/instance', + href: '/docs/core/core-concepts/instance', }, ]; } @@ -97,10 +97,10 @@ export default function Features() {

- The building blocks + The building blocks

- + Six generated pieces cover most of what a world needs. Register or point at something once, then reach it the same way from every behaviour. @@ -116,7 +116,7 @@ export default function Features() { - Details → + Details →

))} diff --git a/src/components/framework/GetStarted.tsx b/src/components/core/GetStarted.tsx similarity index 73% rename from src/components/framework/GetStarted.tsx rename to src/components/core/GetStarted.tsx index 4c20e38..dd79fdd 100644 --- a/src/components/framework/GetStarted.tsx +++ b/src/components/core/GetStarted.tsx @@ -9,20 +9,20 @@ export default function GetStarted() {

- + TsVRC is not affiliated with or endorsed by VRChat Inc.

- + It's also still a work in progress. Every part of it has its own tests, and we run them before every release, but we can't promise we've caught every case. Keep a backup of your project before adding - TsVRC to it or updating it, just to be safe. If something breaks on your end, we + Core to it or updating it, just to be safe. If something breaks on your end, we can't fix it for you, but we'd really like to know about it. Open an issue on GitHub and we'll take a look. @@ -30,19 +30,19 @@ export default function GetStarted() {

- Start building + Start building

- - Add TsVRC through VCC, then follow the getting-started guide to have a behaviour + + Add Core through VCC, then follow the getting-started guide to have a behaviour running in play mode.

- Get started + Get started
diff --git a/src/components/framework/Hero.tsx b/src/components/core/Hero.tsx similarity index 73% rename from src/components/framework/Hero.tsx rename to src/components/core/Hero.tsx index a4d05e3..99e3b54 100644 --- a/src/components/framework/Hero.tsx +++ b/src/components/core/Hero.tsx @@ -1,7 +1,7 @@ import Link from '@docusaurus/Link'; import Translate from '@docusaurus/Translate'; -import AnnotatedCode from '@site/src/components/framework/AnnotatedCode'; -import ScrollCue from '@site/src/components/framework/ScrollCue'; +import AnnotatedCode from '@site/src/components/core/AnnotatedCode'; +import ScrollCue from '@site/src/components/core/ScrollCue'; export default function Hero() { return ( @@ -10,12 +10,12 @@ export default function Hero() {

TsVRC Core

- + A framework for VRChat worlds built on UdonSharp

- + Structured initialization, dependency wiring, and editor codegen, so your world scripts extend a typed base class instead of hand-wiring references every time you add a behaviour. @@ -24,28 +24,28 @@ export default function Hero() {

com.tsvrc.core

- Read the docs + Read the docs - View on GitHub + View on GitHub
diff --git a/src/components/framework/ProblemSolution.tsx b/src/components/core/ProblemSolution.tsx similarity index 64% rename from src/components/framework/ProblemSolution.tsx rename to src/components/core/ProblemSolution.tsx index d25b118..756ebde 100644 --- a/src/components/framework/ProblemSolution.tsx +++ b/src/components/core/ProblemSolution.tsx @@ -5,10 +5,10 @@ export default function ProblemSolution() { return (

- Why it exists + Why it exists

- + Every UdonSharp script starts on its own, with no guarantee of what order things happen in and no easy way to reach another script elsewhere in your world. Most creators solve this by hand: dragging references between objects in the Inspector, @@ -17,18 +17,18 @@ export default function ProblemSolution() {

- how TsVRC fits together + how Core fits together ), code: _ts, }}> - {"TsVRC gives every script one shared reference, {code}, to reach anything else in your project. Setup happens in an order you control, and TsVRC works out which of your own classes to use ahead of time, in the Unity editor. See {link} for the full shape."} + {"Core gives every script one shared reference, {code}, to reach anything else in your project. Setup happens in an order you control, and Core works out which of your own classes to use ahead of time, in the Unity editor. See {link} for the full shape."}

diff --git a/src/components/framework/ScrollCue.tsx b/src/components/core/ScrollCue.tsx similarity index 82% rename from src/components/framework/ScrollCue.tsx rename to src/components/core/ScrollCue.tsx index 71ceb4d..5c44457 100644 --- a/src/components/framework/ScrollCue.tsx +++ b/src/components/core/ScrollCue.tsx @@ -5,7 +5,7 @@ export default function ScrollCue() {
diff --git a/src/components/homepage/Hero.tsx b/src/components/homepage/Hero.tsx index e4a591f..822712c 100644 --- a/src/components/homepage/Hero.tsx +++ b/src/components/homepage/Hero.tsx @@ -1,42 +1,35 @@ import Link from '@docusaurus/Link'; -import useBaseUrl from '@docusaurus/useBaseUrl'; -import Translate from '@docusaurus/Translate'; +import Translate, { translate } from '@docusaurus/Translate'; +// The logo's tile as a band: its charcoal, its near-white, and its blue used once, on the name. export default function Hero() { - const logo = useBaseUrl('img/tsvrc-logo.png'); return ( -
-
- -

- - Tools for building VRChat worlds and avatars - -

-

- - TsVRC builds open-source tools for VRChat creators, starting with a framework for - structuring UdonSharp world code. - -

-
- - Explore TsVRC - - - View on GitHub +
+
+
+ + {translate({ +

+ Toni }}> + {"Hi, I'm {name}"} + +

+

+ + I build tools for making VRChat worlds. TsVRC is the name I publish them under, and + most of them are open source. + +

); diff --git a/src/components/homepage/ProjectCard.tsx b/src/components/homepage/ProjectCard.tsx index b4b3bb6..65b7ce3 100644 --- a/src/components/homepage/ProjectCard.tsx +++ b/src/components/homepage/ProjectCard.tsx @@ -1,37 +1,113 @@ +import { useEffect, useState } from 'react'; import Link from '@docusaurus/Link'; import { translate } from '@docusaurus/Translate'; +import { LawIcon, MarkGithubIcon, StarIcon, TagIcon, type Icon } from '@primer/octicons-react'; export type Project = { name: string; status: 'available' | 'planned'; description: string; - href: string; - cta: string; + docsHref: string; + // The GitHub repository, as "owner/name". + repo: string; }; +type StatKind = 'stars' | 'license' | 'release'; +type Stat = { kind: StatKind; value: string }; + +const statPaths: Record = { stars: 'stars', license: 'license', release: 'v/release' }; +const statIcons: Record = { stars: StarIcon, license: LawIcon, release: TagIcon }; + +function statLabel(kind: StatKind): string { + switch (kind) { + case 'stars': + return translate({ id: 'homepage.projects.stat.stars', message: 'Stars' }); + case 'license': + return translate({ id: 'homepage.projects.stat.license', message: 'License' }); + case 'release': + return translate({ id: 'homepage.projects.stat.release', message: 'Latest release' }); + } +} + +// Shields JSON endpoints accept requests from any site and are cached for 30 minutes, so +// visitors don't use up GitHub's limit of 60 anonymous API requests an hour. Shields reports a +// private or missing repository, or one with no releases, as the value itself; those are left out. +function useRepoStats(repo: string): Stat[] { + const [stats, setStats] = useState([]); + useEffect(() => { + let cancelled = false; + const kinds = Object.keys(statPaths) as StatKind[]; + Promise.all( + kinds.map((kind) => + fetch(`https://img.shields.io/github/${statPaths[kind]}/${repo}.json`) + .then((response) => (response.ok ? response.json() : null)) + .then((badge) => + typeof badge?.value === 'string' && !/not found|no releases|invalid|inaccessible/i.test(badge.value) + ? { kind, value: badge.value } + : null, + ) + .catch(() => null), + ), + ).then((results) => { + if (!cancelled) { + setStats(results.filter((stat): stat is Stat => stat !== null)); + } + }); + return () => { + cancelled = true; + }; + }, [repo]); + return stats; +} + function statusLabel(status: Project['status']): string { return status === 'available' ? translate({ id: 'homepage.projects.status.available', message: 'Available' }) : translate({ id: 'homepage.projects.status.planned', message: 'Planned' }); } +// The whole card links to the project's docs: the title's link stretches over the card, and the +// GitHub link sits above it. export default function ProjectCard({ project }: { project: Project }) { + const stats = useRepoStats(project.repo); return ( -
-
+
+
-

{project.name}

+

+ + {project.name} + +

{statusLabel(project.status)}
-

{project.description}

+ + +
- - {project.cta} → - +

{project.description}

+
    + {stats.map((stat) => { + const StatIcon = statIcons[stat.kind]; + return ( +
  • + + {statLabel(stat.kind)}: + {stat.value} +
  • + ); + })} +
); } diff --git a/src/components/homepage/Projects.tsx b/src/components/homepage/Projects.tsx index bac17aa..92b9743 100644 --- a/src/components/homepage/Projects.tsx +++ b/src/components/homepage/Projects.tsx @@ -4,16 +4,28 @@ import ProjectCard, { type Project } from '@site/src/components/homepage/Project function useProjects(): Project[] { return [ { - name: 'TsVRC', + name: 'Core', status: 'available', description: translate({ - id: 'homepage.projects.tsvrc.description', + id: 'homepage.projects.core.description', message: 'A structured way to build VRChat worlds with UdonSharp: consistent startup, ' + 'wired-up dependencies, and generated code instead of hand-wiring everything yourself.', }), - href: '/framework', - cta: translate({ id: 'homepage.projects.learnMore', message: 'Learn more' }), + docsHref: '/docs/core/intro', + repo: 'tsvrc/tsvrc-core', + }, + { + name: 'Udon Test Kit', + status: 'available', + description: translate({ + id: 'homepage.projects.udonTestKit.description', + message: + 'Automated tests for VRChat worlds: Play Mode tests run your UdonSharp behaviours in a ' + + 'real ClientSim session, with real players.', + }), + docsHref: '/docs/udon-test-kit/intro', + repo: 'tsvrc/udon-test-kit', }, ]; } @@ -21,21 +33,22 @@ function useProjects(): Project[] { export default function Projects() { const projects = useProjects(); return ( -
-

- Projects -

-

- - Every TsVRC project lives on this one site. Just one so far; more will land here as - they ship. - -

-
- {projects.map((project) => ( - - ))} -
-
+
+
+

+ Projects +

+

+ + Everything built so far. Each project has its docs here and its code on GitHub. + +

+
+ {projects.map((project) => ( + + ))} +
+
+
); } diff --git a/src/components/homepage/TrustBar.tsx b/src/components/homepage/TrustBar.tsx deleted file mode 100644 index d651476..0000000 --- a/src/components/homepage/TrustBar.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { translate } from '@docusaurus/Translate'; - -export default function TrustBar() { - return ( -
- ); -} diff --git a/src/css/custom.css b/src/css/custom.css index 0d98d1a..7d1c758 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -36,6 +36,16 @@ html { --ts-danger-fg: #d1242f; --ts-attention-fg: #9a6700; + /* The logo's own tile: charcoal, near-white and one blue. Used for the homepage intro band, + which keeps these colors in both themes, as the logo does. */ + --ts-brand-bg: #161b22; + --ts-brand-fg: #f0f6fc; + --ts-brand-muted: #9198a1; + --ts-brand-accent: #58a6ff; + --ts-brand-dot: rgba(240, 246, 252, 0.08); + --ts-section: #f6f8fa; + --ts-surface: #ffffff; + --ifm-color-primary: #0969da; --ifm-color-primary-dark: #085ec4; --ifm-color-primary-darker: #0758b9; @@ -64,6 +74,9 @@ html { --ts-danger-fg: #f85149; --ts-attention-fg: #d29922; + --ts-section: #0d1117; + --ts-surface: #161b22; + --ifm-color-primary: #58a6ff; --ifm-color-primary-dark: #3994ff; --ifm-color-primary-darker: #298cff; @@ -87,8 +100,20 @@ html { --color-success: var(--ts-success-fg); --color-danger: var(--ts-danger-fg); --color-attention: var(--ts-attention-fg); + --color-brand: var(--ts-brand-bg); + --color-brand-fg: var(--ts-brand-fg); + --color-brand-muted: var(--ts-brand-muted); + --color-brand-accent: var(--ts-brand-accent); + --color-section: var(--ts-section); + --color-surface: var(--ts-surface); --font-heading: 'Space Grotesk', system-ui, sans-serif; --font-body: 'IBM Plex Sans', system-ui, sans-serif; --font-mono: 'IBM Plex Mono', SFMono-Regular, Menlo, Consolas, monospace; -} \ No newline at end of file +} + +/* A faint dot grid, like a scene view's grid, behind the homepage intro. */ +.ts-dot-grid { + background-image: radial-gradient(var(--ts-brand-dot) 1px, transparent 1px); + background-size: 22px 22px; +} diff --git a/src/pages/framework.tsx b/src/pages/core.tsx similarity index 51% rename from src/pages/framework.tsx rename to src/pages/core.tsx index 15b40e3..b5186b0 100644 --- a/src/pages/framework.tsx +++ b/src/pages/core.tsx @@ -1,17 +1,17 @@ import type { ReactNode } from 'react'; import Layout from '@theme/Layout'; import { translate } from '@docusaurus/Translate'; -import Hero from '@site/src/components/framework/Hero'; -import ProblemSolution from '@site/src/components/framework/ProblemSolution'; -import Features from '@site/src/components/framework/Features'; -import GetStarted from '@site/src/components/framework/GetStarted'; +import Hero from '@site/src/components/core/Hero'; +import ProblemSolution from '@site/src/components/core/ProblemSolution'; +import Features from '@site/src/components/core/Features'; +import GetStarted from '@site/src/components/core/GetStarted'; -export default function Framework(): ReactNode { +export default function Core(): ReactNode { return ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e701032..a7577b1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from 'react'; import Layout from '@theme/Layout'; import { translate } from '@docusaurus/Translate'; import Hero from '@site/src/components/homepage/Hero'; -import TrustBar from '@site/src/components/homepage/TrustBar'; import Projects from '@site/src/components/homepage/Projects'; export default function Home(): ReactNode { @@ -11,10 +10,9 @@ export default function Home(): ReactNode { title={translate({ id: 'homepage.meta.title', message: 'TsVRC' })} description={translate({ id: 'homepage.meta.description', - message: 'TsVRC builds open-source tools for VRChat world and avatar creators.', + message: "Toni's open-source tools for making VRChat worlds, published as TsVRC.", })}> - );