This commit is contained in:
Chris Bell 2025-10-17 23:38:04 -05:00
parent ed1827bf04
commit 48d83358b2
2 changed files with 494 additions and 116 deletions

199
readme.md
View File

@ -1,140 +1,107 @@
# SessionZero # SessionZero Client
A free and opensource, systemagnostic TTRPG companion app. SessionZero helps you create, manage, and share tabletop content using an extensible JSON datapack format. It supports fully offline use, and is designed to be selfhostable with future federation capabilities. A free and open-source TTRPG companion app for managing tabletop role-playing games with complete flexibility and control over your data.
## About
## At a glance SessionZero is a cross-platform desktop application built with Godot that helps players and GMs organize their TTRPG sessions. It's designed to be system-agnostic, supporting any tabletop RPG through a modular datapack format.
- Crossplatform desktop client (Avalonia, .NET 9)
- Lightweight ASP.NET server for shared play and sync
- JSON datapacks for datasets, character templates, and session templates
- Offlinefirst; selfhostable; roadmap includes federation and mobile
For a deep dive into goals, architecture, and data formats, see docs/design-doc.md. ### Key Features
- **System-Agnostic**: Works with any TTRPG system through extensible JSON datapacks
- **Full Offline Support**: Play completely offline with local data storage
- **Self-Hosted Multiplayer**: Host sessions directly from your machine with P2P networking
- **Decentralized Identity**: No accounts required - your identity is a cryptographic key pair you control
- **Flexible Networking**: Choose between assisted connection (via optional signaling server) or direct IP connection
- **Open Content Format**: Import and create datapacks for characters, campaigns, items, and more
## Repository layout ## Getting Started
- SessionZero.sln Solution file
- docs/
- design-doc.md Full design overview and datapack specification
- rambling.md Notes and ideas
- src/
- SessionZero.Client/ Avalonia desktop application
- SessionZero.Server/ ASP.NET Core server (minimal API)
- SessionZero.Shared/ Shared models and schema abstractions used by both client and server
- samples/ Placeholder for sample datapacks (.szp) and source folders
- tools/ Project tooling stubs
- license.txt AGPL3.0 license text
## Technology stack
- Language/Runtime: C# on .NET 9.0
- UI: Avalonia (crossplatform desktop)
- Web/Server: ASP.NET Core minimal API, WebSockets planned for realtime
- Data: JSON datapacks for content; PostgreSQL planned for shared online content
## Core concepts (from the design)
- Datapack: Toplevel container (.szp) bundling datasets, templates, and media
- Dataset: Structured records (e.g., items, NPCs). See src/SessionZero.Shared/Models/Dataset.cs
- Template: Reusable structures for characters or sessions
- CharacterTemplate: src/SessionZero.Shared/Models/CharacterTemplate.cs
- SessionTemplate: src/SessionZero.Shared/Models/SessionTemplate.cs
- Schema: Versioned JSON schema ideas captured in docs and src/SessionZero.Shared/Schema/Schema.cs
Read the full specification and examples (including JSON snippets) in docs/design-doc.md, Section 5.
## Datapack basics
A datapack directory typically looks like this:
```text
pack-name/
├── szpack.json # Pack metadata and manifest
├── media/
│ └── images/ # Image assets (referenced by objects)
└── objects/
├── datasets/ # User-defined structured data (e.g. items, NPCs)
├── character-templates/ # Templates defining character structures
└── session-templates/ # Templates defining session/campaign structures
```
Examples of szpack.json, datasets, character templates, and session templates are provided in docs/design-doc.md (sections 5.25.7). The shared C# models in src/SessionZero.Shared mirror these structures.
## Getting started
### Prerequisites ### Prerequisites
- .NET SDK 9.0 or later
- A desktop OS supported by Avalonia (Windows, Linux). Mac may work but is not yet a target.
### Build the solution - Godot 4.5.x (for development)
From the repository root: - Windows or Linux (macOS support planned)
```bash ### Installation
dotnet build SessionZero.sln
```
### Run the desktop client **From Release:**
```bash 1. Download the latest release for your platform
dotnet run --project src/SessionZero.Client 2. Extract and run the executable
```
This launches the Avalonia app.
### Run the server (optional for local/offline; required for shared play) **From Source:**
```bash 1. Clone this repository
dotnet run --project src/SessionZero.Server 2. Open the project in Godot 4.5.x
``` 3. Run the project or export for your platform
The server uses ASP.NET Core default settings and appsettings files under src/SessionZero.Server. Future iterations will add persistence and WebSocket realtime updates.
## How It Works
### Datapacks
Content is organized into **datapacks** (.szp files) - portable archives containing:
- Character templates and sheets
- Campaign/session templates
- Datasets (items, NPCs, spells, etc.)
- Associated media assets
### Hosting Sessions
SessionZero uses a **Host Authority** model where the GM's client acts as the server:
1. **GM** starts a session using a session template
2. **GM** chooses a connection method:
- Generate a Session Key for easy joining (requires signaling server)
- Share their IP/domain for direct connection
3. **Players** join using the Session Key or IP address
4. All session data is stored locally on the GM's machine
### Player Identity
Instead of traditional accounts, SessionZero uses cryptographic key pairs:
- Your **Private Key** (Secret Token) stays on your device and signs all actions
- Your **Public Key** (Client ID) identifies you to session hosts
- Transfer your identity by copying your Private Key to another device
## Using datapacks ## Development Roadmap
- Place or import datapacks (.szp) through the client (UI flows under active development).
- For development and testing, keep source folders that match the structure above, then pack/export to .szp.
- Sample content: see samples/ (placeholder today). The design document shows complete JSON examples you can adapt.
### Phase 1: Core Platform (Current)
- Local session management
- Direct P2P networking
- Datapack system implementation
- Character and campaign management
## Self-hosting and future federation ### Phase 2: Advanced Networking
- Minimal Signaling Server integration
SessionZero is designed to be self-hostable from day one, with a clear roadmap toward optional federation of independently run servers. - UDP hole punching with automatic fallbacks
- SessionZeroDB content discovery
### Self-hosted server (today)
- Self-hosted by design: run the SessionZero server locally or on your own VPS; no central service is required to play with friends you invite. The desktop client works fully offline; the server is only needed for shared play and sync.
- Data ownership: datapacks and saves live on your disk. For online/shared play, your own server holds your data. Planned persistence for the server is PostgreSQL; early builds may use in-memory or file-backed storage.
- How to self-host (early stage):
1. Run the server: `dotnet run --project src/SessionZero.Server`
2. Expose it on your LAN or behind a reverse proxy (e.g., Nginx/Caddy) with TLS.
3. Configure `appsettings.json` under `src/SessionZero.Server` for host/ports and, when available, database connection strings.
4. Point clients to your server URL (UI setting to be added; defaults are `http://localhost`).
- Security and licensing notes:
- Use HTTPS when exposing publicly; prefer a reverse proxy and strong auth when available.
- The code is AGPL-3.0: if you modify and run a public instance, you must make the modified source available to its users.
### Federation (future/Phase 2)
- Goal: independent servers discover and share public content (datapacks, datasets, templates) across a trust network.
- Identity: users have global IDs; datapacks are identified by UUID.
- Discovery and sharing: opt-in directories/search; servers fetch and cache public content from peers you trust.
- Permissions and privacy: private games stay private; only content you mark public is advertised.
- Interop and versioning: content references remain stable across servers via IDs and versions.
See docs/design-doc.md, Section 7 for more architectural detail.
## Roadmap (high level)
- Phase 1: Core Platform (client UI, server scaffolding, datapack format)
- Phase 2: Federation (crossserver discovery and sharing)
- Phase 3: Mobile and Plugins (broader platform support and extensibility)
Details for each phase are in docs/design-doc.md, Section 9.
### Phase 3: Mobile & Extensions
- Web (WASM) application
- Mobile applications
- Plugin system and API extensions
## Contributing ## Contributing
Contributions are welcome! Suggested ways to help:
- Try building/running the client and server; report issues and UX rough edges.
- Create example datapacks that exercise datasets and templates.
- Discuss schemas and validation approaches (see docs/design-doc.md, Section 5.8).
Before submitting a PR, please ensure the solution builds with .NET 9.0. Tests and CI will be added as the project matures. Contributions are welcome! Please read our contributing guidelines before submitting pull requests.
## Related Projects
- **SessionZero MSS**: Minimal Signaling Server for assisted P2P connections (separate repository)
- **SessionZeroDB**: Federated content indexing service (planned)
## License ## License
Source code is licensed under the GNU Affero General Public License v3.0 (AGPL3.0). See license.txt.
Usergenerated content is intended to be shareable under Creative Commons BYSA 4.0; see docs/design-doc.md, Section 8.2, for the rationale. - **Source Code**: AGPL-3.0 (Affero General Public License)
- **User Content**: Creative Commons BY-SA 4.0
## Support
For issues, feature requests, or questions:
- Open an issue on GitHub
- Visit the website [SessionZero.app](https://sessionzero.app)
- Check the full design document [Here](sessionzero-design-doc-wip.md)
- Join the community on [Discord](https://discord.gg/RgG5HsJNMT)
---
**Note**: SessionZero is in active development. Features and APIs may change as the project evolves.

View File

@ -0,0 +1,411 @@
# SessionZero Design Document
## 1. Introduction
### 1.1 Purpose
SessionZero is a free and open-source TTRPG companion app designed to streamline tabletop role-playing game management. It emphasizes flexibility, modularity, and federation through a **system-agnostic datapack format**, supporting both offline and self-hostable online play. The goal is to empower players and GMs with full control over their data, rulesets, and session management.
### 1.2 Scope
The project will begin as a cross-platform desktop application (Windows and Linux) built primarily with Godot/GDScript. This client application can also function as the **Host Authority** (server) for a session. The architecture is decentralized: user accounts are replaced with a **cryptographic key-pair identity model**, and all session data is stored **locally on the GM's machine**. The centralized content database (SessionZeroDB) is decoupled and planned as a separate, federated content indexing project in a later phase.
### 1.3 Objectives
- Provide a modular, open-source companion app for any TTRPG system.
- Implement a JSON-based datapack format for extensible content storage.
- Support full offline use and resilient, self-hostable online sessions.
- Implement a decentralized, token-based identity system.
- Create a foundation for future content federation and mobile support.
## 2. System Overview
SessionZero operates with three main layers designed for independence and resilience:
1. **Frontend (Client/Host Authority):** The Godot application running on every machine. It serves as the primary UI, the Datapack consumer, and, when hosting a game, acts as the authoritative session server utilizing Godot's High-Level Multiplayer (HLM) system.
2. **Backend (Optional Signaling Service):** A minimal, open-source **Minimal Signaling Server (MSS)** available for easy peer discovery. This service handles ephemeral connection brokering but stores no game data or persistent identity information. **This layer is completely optional**, as users can establish direct P2P connections without any signaling service.
3. **Datapack System:** A structured archive format (.szpack) for content, with a fully decoupled content federation strategy (SessionZeroDB) planned for later phases.
## 3. Technology Stack
- **Client/Host:** Godot (cross-platform desktop) and GDScript. Godot HLM is used for all session communication (P2P/UDP).
- **Signaling Server:** Go (Golang) for the minimal, high-performance MSS (Minimal Signaling Server).
- **Database/Persistence:** **Local file storage, Godot Resources, and SQLite** for saving session state, character instances, and the Host's player identity data.
- **Networking:** Hybrid P2P (UDP Hole Punching or UPnP fallback) or a direct P2P (UPnP/port-forward) option.
## 4. Core Data Concepts
### 4.1 Datapack
The top-level container (.szp file), bundling all system-specific data (templates, datasets, assets).
### 4.2 Template
A reusable definition of a data structure (e.g., a **Character Template** defining character sheet fields, or a **Session Template** defining campaign structure).
### 4.3 Dataset
A structured collection of external data records (e.g., a bestiary, a list of items, or spells).
### 4.4 Instance
The runtime data created from a Template (e.g., "Aelric's Character Sheet" based on the "Fantasy Character" Template).
### 4.5 Schema
Formal JSON Schemas used to validate the structure of Datapacks and their contents.
## 5. Datapack Specification
A datapack is a self-contained archive (.szp) that holds all data and assets for a specific TTRPG system, module, or expansion. It serves as the foundation for SessionZero's extensible content model. Each pack can define **datasets**, **templates**, and optionally contain instance data (though instances are usually stored separately as save files). The structure emphasizes modularity, portability, and reliable dependency management between packs.
### 5.1 Directory Structure
```
pack-name/
├── szpack.json # Pack metadata and manifest
├── media/
│ └── images/ # Image assets (referenced by objects)
└── objects/
├── datasets/ # User-defined structured data (e.g. items, NPCs)
├── character-templates/ # Templates defining character structures
└── session-templates/ # Templates defining session/campaign structures
```
### 5.2 Pack Metadata (szpack.json)
Each datapack contains a root metadata file that defines its identity, authorship, dependencies, and compatibility information. This structure aligns with the C# Datapack and DatapackDependency models.
```json
{
"id": "c9a3f9c8-8b8a-4f1f-9f3b-2d8c7f2c6c62",
"name": "Example Pack",
"version": "1.0.0",
"author": "UserName",
"license": "CC-BY-SA-4.0",
"description": "A fantasy setting datapack containing items and NPCs.",
"createdAt": "2025-10-15T00:00:00Z",
"sessionZeroVersion": "1.0.0",
"dependencies": [
{
"id": "c2e6d0a4-2a8f-4a51-b36e-9f8f2c7b1d11",
"name": "Core-Ruleset",
"version": "1.2.0"
}
]
}
```
### 5.3 Common Object Metadata
Objects within the datapack (datasets, templates) inherit from SzObject. The szType field is required to identify the object type. Image paths are relative to the media/images folder.
```json
{
"id": "core-items",
"name": "Core Items",
"szType": "dataset",
"description": "A collection of basic weapons and armor.",
"icon": "core-items.png",
"version": "1.0.0",
"schemaVersion": "1.0.0"
}
```
*Note: The system resolves the icon field path (e.g., "core-items.png") to datapack/media/images/core-items.png.*
### 5.4 Dataset Objects
Datasets are structured collections of data entries. The entry structure includes TopLevelFields and optional Groups of fields, aligning with the DatasetEntry C# model.
```json
{
"id": "core-items",
"szType": "dataset",
"datasetType": "items",
"name": "Core Items",
"description": "Weapons, armor, and consumables for the base ruleset.",
"version": "1.0.0",
"schemaVersion": "1.0.0",
"entries": {
"sword": {
"id": "sword",
"name": "Sword",
"description": "A basic weapon.",
"icon": "weapons/sword.png",
"topLevelFields": {
"damage": { "type": "Number", "value": 10 },
"weight": { "type": "Number", "value": 3 }
},
"groups": [
{
"id": "stats",
"name": "Stats",
"fields": {
"rarity": { "type": "Text", "value": "Common" }
}
}
]
},
"potion": {
"id": "potion",
"name": "Healing Potion",
"description": "Restores a small amount of HP.",
"icon": "consumables/potion.png",
"topLevelFields": {
"healAmount": { "type": "Number", "value": 20 },
"consumable": { "type": "Boolean", "value": true }
}
}
}
}
```
*Note: The field retrieval path is expected to be EntryName.FieldName or EntryName.GroupName.FieldName, matching the logic in Dataset.cs.*
### 5.5 Character Templates
Character templates define the fields and structure for characters. Note the use of the specific **DatasetLink** object for list fields, requiring a Datapack ID, Dataset ID, and Version.
```json
{
"id": "default-character",
"szType": "character-template",
"name": "Generic Character",
"description": "A base character layout usable across multiple systems.",
"icon": "character.png",
"version": "1.0.0",
"schemaVersion": "1.0.0",
"sections": [
{
"id": "core",
"name": "Core Stats",
"groups": [
{
"id": "attributes",
"name": "Attributes",
"fields": [
{ "id": "strength", "name": "Strength", "type": "Number", "defaultValue": 10 },
{ "id": "dexterity", "name": "Dexterity", "type": "Number", "defaultValue": 10 },
{ "id": "intelligence", "name": "Intelligence", "type": "Number", "defaultValue": 10 }
]
},
{
"id": "inventory",
"name": "Inventory",
"fields": [
{
"id": "equipment",
"name": "Equipment",
"type": "List",
"datasetLink": {
"datapackId": "c9a3f9c8-8b8a-4f1f-9f3b-2d8c7f2c6c62",
"datasetId": "core-items",
"version": "1.0.0"
}
}
]
}
]
}
]
}
```
### 5.6 Session Templates
Session templates are blueprints for setting up a campaign. This example uses **CharacterTemplateLink** and a list of **DatasetLink** objects for **RequiredDatasets** as defined in SessionTemplate.cs.
```json
{
"id": "basic-session",
"szType": "session-template",
"name": "Basic Fantasy Campaign",
"description": "A classic campaign setup with standard fantasy rules.",
"icon": "session.png",
"version": "1.0.0",
"schemaVersion": "1.0.0",
"characterTemplateLink": {
"datapackId": "c9a3f9c8-8b8a-4f1f-9f3b-2d8c7f2c6c62",
"templateId": "default-character",
"version": "1.0.0"
},
"requiredDatasets": [
{
"datapackId": "c9a3f9c8-8b8a-4f1f-9f3b-2d8c7f2c6c62",
"datasetId": "core-items",
"version": "1.0.0"
}
],
"sections": [
{
"id": "session-info",
"name": "Session Info",
"groups": [
{
"id": "details",
"name": "Details",
"fields": [
{ "id": "setting", "name": "Setting", "type": "Text", "defaultValue": "Thalindra" },
{ "id": "gmNotes", "name": "GM Notes", "type": "MultiText" }
]
}
]
}
]
}
```
### 5.7 Instances
Instances, such as a specific character or a running campaign's state, are stored outside of the core, reusable datapacks and are instead stored in the local db/filesystem.
### 5.8 Validation and Schema Versioning
Formal, versioned JSON Schemas define the structure of szpack.json and core object types.
- Each object declares a **schemaVersion** (global versioning per SessionZero release).
- Validation tools confirm compliance before import/export.
- Internal references (DatasetLink, CharacterTemplateLink, etc.) are resolved within the pack automatically.
- External references are verified via dependencies in szpack.json.
**Session Datapack vs Templates:**
- **Session Template:** A reusable blueprint for campaign/session setup. Lives under objects/session-templates/.
- **Session Datapack:** The persisted, evolving state of an ongoing campaign. It is stored as a special pack and does not appear alongside normal content packs in the UI. Snapshots are versioned with timestamps. Export/import uses the same .szp format with a distinct type flag in szpack.json.
## 6. Networking & Synchronization
### 6.1 Session Host Authority
A "Session" is a single game instance, always hosted by one GM whose client is running as the **Host Authority**. The Host is responsible for state validation, conflict resolution, and persistence to its local storage. All connected clients sync their state with this central Host.
### 6.2 Dual Connection System
SessionZero provides two independent connection methods, giving users full control over how they establish multiplayer sessions. Both methods use P2P connections with automatic fallback mechanisms, ensuring the application remains functional regardless of network configuration.
#### 6.2.1 Signaling Server Connection (MSS Method)
The first connection method uses a **Minimal Signaling Server (MSS)** to facilitate peer discovery and connection establishment:
**Process Flow:**
1. The Godot Host generates a short, temporary **Session Key** (e.g., ALPHA-2345)
2. The Host determines its external connection addresses (UDP/WS) and registers them with a configured MSS
3. Players enter the Session Key in their client to retrieve the Host's connection information
4. Connection is attempted using UDP Hole Punching for direct P2P communication
5. If UDP Hole Punching fails, the client automatically falls back to:
- UPnP (Universal Plug and Play) if the router supports it
- Direct connection if the port is already forwarded
6. If all connection methods fail, the player is notified and cannot join
**MSS Configuration:**
- Users can select which MSS to use in application settings
- An official MSS instance will be provided by the project
- The MSS source code and binaries are distributed for self-hosting
- The MSS is ephemeral and stateless, immediately deleting Session Keys after lookup
**Use Case:** This method is ideal for users who want easy session setup without manual network configuration, while still maintaining the option for self-hosted infrastructure.
#### 6.2.2 Direct Connection Method
The second connection method completely bypasses the MSS layer, establishing direct P2P connections using the Host's IP address or domain name:
**Process Flow:**
1. The GM provides players with their public IP address, domain name, or tunnel endpoint (e.g., ngrok, Cloudflare Tunnel)
2. Players enter this address directly into their client
3. Connection is attempted using:
- UPnP (Universal Plug and Play) if the router supports it
- Direct connection if the port is already forwarded
4. If neither method succeeds, the player is notified and cannot join
**Requirements:**
- The GM must have either:
- A router with working UPnP enabled
- Manually forwarded ports on their router
- A tunneling service configured (ngrok, Cloudflare Tunnel, etc.)
**Use Case:** This method is crucial for users who prefer complete independence from any third-party services (including self-hosted MSS instances), have their own networking infrastructure, or simply want the most direct connection path possible.
### 6.3 Connection Method Independence
These two connection methods are **completely independent** of each other:
- The Direct Connection method is **not** a fallback for MSS connection failures
- Users choose their preferred method based on their needs and infrastructure
- Either method ensures full application functionality
- Sessions using either method have identical features and performance characteristics
This dual-mode approach guarantees that SessionZero remains resilient and accessible regardless of:
- MSS availability or outages
- Network configuration complexity
- User preference for independence vs. convenience
- Corporate or restrictive network environments
### 6.4 Data Flow
1. **Client Action:** A player (or the GM) updates their character's health in their local client.
2. **Client:** The change request is signed with the Player's Private Key and pushed to the **Host Authority** via the established P2P connection (regardless of connection method used).
3. **Host Authority (Godot):** Validates the request (checking signature against stored Public Key), commits the change to its local session data, and broadcasts the updated state to all connected players (including the originator).
4. **Players:** Receive the updated session state and apply it to their local model.
### 6.5 Offline Mode
The application supports full offline functionality, using local storage. When connectivity is restored, the system focuses on session hosting or joining, with conflict-resolution being managed by the Host Authority during session play.
## 7. Identity and Federation
### 7.1 Decentralized Player Identity
Users do not have global accounts. Player identity is managed by a cryptographic key pair, granting players full control over their identity and enabling cryptographically verifiable actions.
- **Identity Generation:** On first client launch, a permanent **Private Key** (Secret Token) and corresponding **Public Key** (Client ID) are generated and stored locally.
- **Private Key (Secret Token):** A long, complex string. It is stored securely and *never* transmitted, only used to sign actions. Players can copy this Private Key to transfer their identity and associated character data to another device.
- **Public Key (Client ID):** A shorter, safe-to-share identifier. This is sent to the Host upon connection. The Godot Host stores this Public Key linked to the player's character data to authenticate signatures.
- **Authentication:** All communication with the Host is digitally signed by the Player's Private Key and verified by the Host against the stored Public Key.
### 7.2 SessionZeroDB (Future Decoupled Content Federation)
The large-scale, shared content database concept is decoupled into an entirely separate, future project layer focused on content discovery and distribution, aligning with the federation goals.
- **Content Discovery:** This separate project will define APIs and protocols (potentially using RSS-like feeds and WebTorrent/IPFS principles) to index **manifests** of public Datapacks.
- **Dependency Fulfillment:** The core client will use this federated layer to discover, validate, and download Datapacks and dependencies directly from third-party hosting maintainers (self-hosted servers).
## 8. Licensing
### 8.1 Source Code License
- **AGPL-3.0 (Affero General Public License):** Ensures open-source continuity and attribution, prevents uncredited commercial rebranding.
### 8.2 User Content License
- **Creative Commons BY-SA 4.0:** Allows sharing and adaptation with attribution.
## 9. Development Phases
### Phase 1: Core Platform (MVP)
- Client (UI, datapack editing, local session management)
- **Networking** implementation with the direct connection method:
- Direct P2P connection using IP/domain with UPnP and manual port-forward support
- **Decentralized Player Identity** implementation (key pair generation, signing, and host verification)
- JSON datapack format implementation and validation tools
### Phase 2: Advanced Networking & Content Distribution
- Minimal Signaling Server (MSS) implementation (Written in Golang)
- **Networking** implementation of the MSS-mediated connection method:
- MSS-mediated connection with UDP Hole Punching and automatic fallbacks (UPnP, direct)
- Connection stability monitoring and diagnostic tools for both connection methods
- Initial work on the separate SessionZeroDB API/indexing service for content discovery and download
- Automatic datapack dependency fulfillment via the SessionZeroDB
### Phase 3: Mobile and Plugins
- Mobile apps with adapted connection workflows
- Plugin and API extensibility
## 10. Accessibility & Internationalization
- **Accessibility:** Keyboard navigation across all primary views, sufficient color contrast, focus indicators, and screen-reader-friendly labels.
- **Internationalization:** Resource-based localization for UI strings; UTF-8 throughout; prepare for RTL support; date/number formatting via invariant + locale-aware overlays.