diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..0f243d7
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,5 @@
+{
+ "enabledPlugins": {
+ "superpowers@claude-plugins-official": true
+ }
+}
diff --git a/.gitignore b/.gitignore
index 48a955f..445e81e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ src/Perpetuum/bin/
src/Perpetuum.ServerService2/Properties/PublishProfiles/
*.user
packages/
+!src/Perpetuum.AdminTool/Packages/
src/Perpetuum.ServerService2Installer/bin/
src/Perpetuum.ServerService2/data/bitmaps/
src/Perpetuum.ServerService2/data/chatlogs/
@@ -18,3 +19,5 @@ src/Perpetuum.ServerService2/data/layers/
src/Perpetuum.ServerService2/data/logs/
bin/
Releases/
+.claude/settings.local.json
+.planning/
diff --git a/.superpowers/brainstorm/152-1778423504/content/detail-tabs.html b/.superpowers/brainstorm/152-1778423504/content/detail-tabs.html
new file mode 100644
index 0000000..6ebc2a3
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/detail-tabs.html
@@ -0,0 +1,122 @@
+
Design Section 3 — Season Detail Tabs
+After clicking "Manage →" on a season card. Tab bar at top, each section is an editable DataGrid or form. Changes go to the change queue on edit.
+
+
+
+
+
+
+
+
← All Seasons
+
Season 3
+
● Active
+
+
Deactivate
+
Activate
+
+
+
+
+ General
+ Activity Rates
+ Objectives
+ Tiers
+ Leaderboard
+ Packages
+ 📊 Statistics
+
+
+
+
+
+
+
Define point thresholds that unlock reward packages. Tiers must be in ascending order by points required.
+
+ Add Tier
+
+
+
+
+
+ # Tier Name Points Required Reward Package Actions
+
+
+
+
1
+
Stone
+
1,000
+
▾ Starter Pack
+
+ ✎
+ ✕
+
+
+
+
+
2
+
Bronze
+
5,000
+
▾ Bronze Reward
+
+ ✎
+ ✕
+
+
+
+
+
3
+
Silver
+
15,000
+
▾ Silver Reward
+
+ ✎
+ ✕
+
+
+
+
+
4
+
Gold
+
35,000
+
▾ Gold Reward
+
+ ✎
+ ✕
+
+
+
+
+
Changes are queued — commit via the Pending Changes tab when ready.
+
+
+
+
+
+All Tabs at a Glance
+
+
+
General
+
Name, description, start/end dates. Activate / Deactivate buttons with confirmation dialog. Season ID shown read-only.
+
+
+
Activity Rates
+
Same grid as wizard Step 2 — all 8 types, editable pts/unit and scale, live effective rate label.
+
+
+
Objectives
+
DataGrid: name, activity type (dropdown), target value, bonus points, display order. Add/edit via inline row or dialog. Delete with confirmation.
+
+
+
Tiers ← shown above
+
DataGrid: tier #, name, points required, package (dropdown). Add/delete rows. Edit inline.
+
+
+
Leaderboard
+
DataGrid: rank min, rank max, package (dropdown). Validation: ranges must not overlap. Gap warning if ranks are skipped.
+
+
+
Packages
+
Same as the top-level Packages view, scoped for convenience. Shows all packages; highlights which ones are used by this season's tiers/leaderboard.
+
+
+
+Does the detail view structure look right?
diff --git a/.superpowers/brainstorm/152-1778423504/content/detail-view.html b/.superpowers/brainstorm/152-1778423504/content/detail-view.html
new file mode 100644
index 0000000..0beeb87
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/detail-view.html
@@ -0,0 +1,75 @@
+Season Detail View
+After clicking "Manage →" on a season card — how should the detail be structured?
+
+
+
+
+
+
A
+
+
Tabbed Sections (Recommended)
+
Full-screen detail with a tab bar across the top. Each config area is its own tab. Clean, familiar, matches the wizard steps. Back button returns to cards.
+
+
+
+
+
← All Seasons
+
Season 3
+
● Active
+
+
Deactivate
+
+
+ General
+ Activity Rates
+ Objectives
+ Tiers
+ Leaderboard
+ Packages
+ 📊 Statistics
+
+
+
Name Season 3
+
Description Spring 2026 Season
+
Start Time 2026-03-01 00:00
+
End Time 2026-05-31 23:59
+
+
+
+
+
+
+
+
+
B
+
+
Accordion Sections
+
All config sections stacked vertically, expand/collapse individually. Good for reviewing everything at once, but can get long with many objectives or tiers.
+
+
+
+
+ ← All Seasons
+ Season 3
+ ● Active
+
+
+
+
▼ General Info
+
+
Name Season 3
+
Start 2026-03-01
+
+
+
▶ Activity Rates 8 configured
+
▶ Objectives 3 configured
+
▶ Tiers 4 tiers
+
▶ Leaderboard Rewards 3 brackets
+
▶ 📊 Statistics
+
+
+
+
+
+
+
diff --git a/.superpowers/brainstorm/152-1778423504/content/management-approaches.html b/.superpowers/brainstorm/152-1778423504/content/management-approaches.html
new file mode 100644
index 0000000..b55748a
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/management-approaches.html
@@ -0,0 +1,114 @@
+Seasons Management Layout
+How should the management view look for existing seasons? Pick an approach — or describe what you'd change.
+
+
+
+
+
+
A
+
+
Master-Detail (Recommended)
+
Season list on the left, tabbed detail panel on the right — mirrors the existing EntitiesView pattern. Familiar, compact, lets you switch seasons quickly.
+
+
+
+
+
Seasons
+
+ ● Season 3 ACTIVE
+
+
Season 2
+
Season 1
+
+ New Season
+
+
+
+ General
+ Activity Rates
+ Objectives
+ Tiers
+ Leaderboard
+ Packages
+ Statistics
+
+
+
Name Season 3
+
Description Spring 2026
+
Start 2026-03-01 00:00
+
End 2026-05-31 23:59
+
Status ● Active
+
+
+
+
+
+
+
+
+
+
B
+
+
Accordion Sections
+
Season selector at the top, then all config sections stacked below as expandable panels. You see everything for one season at once; collapse sections you're not working on.
+
+
+
+
+
Season:
+
▾ Season 3 (ACTIVE)
+
+ New Season
+
+
+
▶ General Info Season 3 · 2026-03-01 → 2026-05-31
+
+
▼ Activity Rates 8 types configured
+
+ Activity Pts/Unit Scale
+ NPC Kill 10 1
+ PvP Kill 50 1
+ Mining 1 1000
+
+
+
▶ Objectives 3 configured
+
▶ Tiers 4 tiers
+
▶ Leaderboard Rewards 3 brackets
+
+
+
+
+
+
+
+
+
C
+
+
Dashboard Cards
+
Overview page with season cards (active season highlighted). Clicking a card opens a full-page detail view. More visual, but requires navigation back and forth.
+
+
+
+
+
+
● ACTIVE
+
Season 3
+
Ends: 2026-05-31
+
1,247 participants
+
Manage →
+
+
+
ENDED
+
Season 2
+
Mar–Apr 2026
+
892 participants
+
View →
+
+
+
+
+
+
+
+
+
diff --git a/.superpowers/brainstorm/152-1778423504/content/packages-view.html b/.superpowers/brainstorm/152-1778423504/content/packages-view.html
new file mode 100644
index 0000000..3d8ea94
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/packages-view.html
@@ -0,0 +1,102 @@
+Design Section 4 — Packages View
+Accessed via the "Packages" switcher in the Seasons tab header. Master-detail: package list left, items right. Mirrors the existing EntitiesView pattern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gold Reward
+
3 items · Used by 2 seasons
+
+
+
Silver Reward
+
2 items · Used by 2 seasons
+
+
+
Bronze Reward
+
2 items · Used by 1 season
+
+
+
Starter Pack
+
4 items · Used by 3 seasons
+
+
+
Leaderboard Top 3
+
5 items · Used by 1 season
+
+
+
Unused Prototype
+
1 item · Not used
+
+
+
+
+
+
+
+
+
Gold Reward
+
Used by Season 2 (Tier 4) · Season 3 (Tier 4)
+
+
+
+ Add Item
+
Delete Package
+
+
+
+
+
+ ⚠ This package is used by an active season. Changes will affect players who have not yet claimed this tier reward.
+
+
+
+
+
+ Item (Definition) Quantity Is Stackable Del
+
+
+
Syndicate Veteran Token
+
+
Yes
+
✕
+
+
+
Prototype Frame (Light)
+
+
No
+
✕
+
+
+
NIC Voucher (500,000)
+
+
Yes
+
✕
+
+
+
Items are pulled from the entity definitions. Quantities are editable. Changes are queued.
+
+
+
+
+
+Package list shows item count and which seasons use each package. The detail warns you if you're editing a package used by an active season. Does this look right?
diff --git a/.superpowers/brainstorm/152-1778423504/content/statistics-tab.html b/.superpowers/brainstorm/152-1778423504/content/statistics-tab.html
new file mode 100644
index 0000000..23fddf0
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/statistics-tab.html
@@ -0,0 +1,149 @@
+Statistics Tab Layout
+Proposed metrics for both balance tuners and community managers. Does this cover what you need, or are there gaps?
+
+
+
+
+
+
+
Participation Health
+
+
+
1,247
+
Total Participants
+
+
+
389
+
Active (last 7 days)
+
+
+
18d 4h
+
Time Remaining
+
+
+
31%
+
Retention (7d active / total)
+
+
+
+
+
+
+
+
Tier Distribution
+
+
+
Tier 4 · Gold 47 players (3.8%)
+
+
+
+
Tier 3 · Silver 183 players (14.7%)
+
+
+
+
Tier 2 · Bronze 412 players (33%)
+
+
+
+
Tier 1 · Stone 605 players (48.5%)
+
+
+
+
+
+
+
+
Top 10 Leaderboard
+
+
+ # Character Points
+
+
+ 1 Aethon Prime 48,210
+
+
+ 2 Krixath 41,780
+
+
+ 3 Velindra 39,950
+
+
+ 4 Solvax 31,200
+
+
+ 5–10 … 18k–28k
+
+
+
+
+
+
+
Balance Tuning
+
+
+
+
+
+
Points by Activity Type
+
+ Activity Total Pts Avg/Player
+
+
+ NPC Kill 12.4M 9,942
+
+
+ Mining 8.1M 6,497
+
+
+ NIC Earned 5.7M 4,571
+
+
+ PvP Kill 2.2M 1,764
+
+
+ Mission / EP / NIC Spent / Intrusion … …
+
+
+
+
+
+
+
+
Avg Points per Day (All Players)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
~3,200 pts/day avg this week · Trend: stable
+
+
+
Objective Completion Rates
+
+ Kill 50 NPCs 78%
+ Mine 10k Ore 44%
+ Win 5 PvP 12%
+
+
+
+
+
+
+
+ ⚡ Balance Insight:
+ At the current average velocity (3,200 pts/day), a new player joining today would reach Tier 2 in ~8 days and Tier 3 in ~22 days. Top 1% are accumulating 5× faster than the median. Consider adjusting PvP Kill rate — only 12% of players engage with it.
+
+
+
+
+
+Does this cover the metrics you need? Let me know what to add, remove, or change.
diff --git a/.superpowers/brainstorm/152-1778423504/content/tab-overview.html b/.superpowers/brainstorm/152-1778423504/content/tab-overview.html
new file mode 100644
index 0000000..cb38bee
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/tab-overview.html
@@ -0,0 +1,75 @@
+Design Section 1 — Seasons Tab Overview
+The Seasons tab has two top-level views. A segmented header switches between them.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
● ACTIVE
+
Season 3
+
2026-03-01 → 2026-05-31
+
18d 4h remaining
+
+
1,247 players
+
4 tiers
+
+
Manage →
+
+
+
+
+
ENDED
+
Season 2
+
2026-01-01 → 2026-02-28
+
Ended 71 days ago
+
+
892 players
+
3 tiers
+
+
View →
+
+
+
+
+
DRAFT
+
Season 4
+
2026-06-01 → 2026-08-31
+
Not yet activated
+
+
Edit →
+
+
+
+
+
+
+
New Season
+
Opens setup wizard
+
+
+
+
+
+
+ Switch to Packages view to create and manage reward packages before wiring them to tiers.
+
+
+
+
+
+Seasons view shows cards: Active (highlighted blue), Draft (dashed, not yet activated), Ended (dimmed). The Packages view switcher lives in the top-left. Does this structure look right?
diff --git a/.superpowers/brainstorm/152-1778423504/content/welcome.html b/.superpowers/brainstorm/152-1778423504/content/welcome.html
new file mode 100644
index 0000000..4ee510d
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/welcome.html
@@ -0,0 +1,5 @@
+Seasons Admin Tool — Design Session
+Visual companion is ready. Layout mockups and design options will appear here as we work through the design.
+
+
Continuing in terminal…
+
diff --git a/.superpowers/brainstorm/152-1778423504/content/wizard-flow.html b/.superpowers/brainstorm/152-1778423504/content/wizard-flow.html
new file mode 100644
index 0000000..a19f702
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/content/wizard-flow.html
@@ -0,0 +1,149 @@
+Design Section 2 — Creation Wizard
+A 6-step wizard opened by clicking "+ New Season". Steps are shown as a progress bar at the top. You can go Back freely; Next validates the current step before advancing.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Activity Rates
+
+ Set how many season points players earn for each in-game activity. Points per Unit is the reward per one unit of activity. Scale groups raw amounts before multiplying — e.g., Scale 1000 means "per 1000 NIC earned" rather than per 1 NIC. Leave a rate at 0 to disable that activity type for this season.
+
+
+
+
+
+
+ Activity Type Pts / Unit Scale Effective rate (example)
+
+
+
NPC Kill
+
+
+
10 pts per kill
+
+
+
PvP Kill
+
+
+
50 pts per kill
+
+
+
Mission Complete
+
+
+
25 pts per mission
+
+
+
Mineral Mined
+
+
+
1 pt per 1,000 units mined
+
+
+
EP Spent
+
+
+
5 pts per 100 EP spent
+
+
+
NIC Earned
+
+
+
1 pt per 10,000 NIC earned
+
+
+
NIC Spent
+
+
+
Disabled (0 pts)
+
+
+
Intrusion Point
+
+
+
100 pts per intrusion point
+
+
+
+
+
+
← Back
+
Step 2 of 6 · Objectives and Tiers are optional but recommended
+
Next: Objectives →
+
+
+
+
+
+
+
Wizard Steps Summary
+
+
+
Step 1 · Season Info
+
Name, description, start date, end date. Validates that end > start.
+
+
+
Step 2 · Activity Rates
+
All 8 activity types pre-listed. Pts/unit + scale per row. Live "effective rate" label. Set to 0 to disable.
+
+
+
Step 3 · Objectives (optional)
+
Add milestone objectives. Each has name, activity type, target count, bonus points. Can skip.
+
+
+
Step 4 · Tiers (optional)
+
Tier name, point threshold, package picker (dropdown from existing packages). Warning shown if no packages exist yet.
+
+
+
Step 5 · Leaderboard (optional)
+
Rank brackets (min–max) each linked to a package. Can skip if no competitive rewards planned.
+
+
+
Step 6 · Review & Queue
+
Read-only summary of all config. "Add to Change Queue" button. Season is created as a Draft — admin activates it separately.
+
+
+
+
+Does the wizard flow make sense? Any steps to add, merge, or reorder?
diff --git a/.superpowers/brainstorm/152-1778423504/state/server-stopped b/.superpowers/brainstorm/152-1778423504/state/server-stopped
new file mode 100644
index 0000000..9582949
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/state/server-stopped
@@ -0,0 +1 @@
+{"reason":"idle timeout","timestamp":1778426625424}
diff --git a/.superpowers/brainstorm/152-1778423504/state/server.pid b/.superpowers/brainstorm/152-1778423504/state/server.pid
new file mode 100644
index 0000000..492dff0
--- /dev/null
+++ b/.superpowers/brainstorm/152-1778423504/state/server.pid
@@ -0,0 +1 @@
+152
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..53c015b
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,384 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+# What This Is
+
+Open Perpetuum Server 2 is an MMO game server for the Perpetuum online game.
+
+Technology:
+- .NET 8
+- C# 12
+- SQL Server
+- x64 only
+- Windows only
+
+---
+
+# Instruction Priority
+
+When instructions conflict, prioritize:
+
+1. Correctness and safety
+2. Existing architecture consistency
+3. Database/documentation accuracy
+4. Runtime stability and performance
+5. Minimal change scope
+6. Coding style consistency
+
+---
+
+# Core Engineering Principles
+
+- Don't assume. Surface uncertainty explicitly.
+- Prefer minimal, focused changes.
+- Touch only what is necessary.
+- Preserve existing architecture and runtime assumptions.
+- Reuse existing patterns before introducing new abstractions.
+- Prefer consistency over novelty.
+- Avoid speculative refactors.
+- Keep naming and placement consistent with surrounding code.
+- Do not make commits unless explicitly asked.
+
+Update:
+- `.claude/knowledge/architecture.md`
+
+when introducing major architectural changes.
+
+---
+
+# Build & Run
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+
+cd src/Perpetuum.Server
+dotnet run -- --GameRoot "E:\PerpetuumServer2\data"
+```
+
+CI:
+- `.github/workflows/dotnet.yml`
+
+Output:
+- `bin/x64/Release/net8.0`
+
+There are currently no automated tests.
+
+---
+
+# Authoritative Documentation
+
+The `docs/` directory is the authoritative source of truth.
+
+## Architecture
+- `docs/codebase/ARCHITECTURE.md`
+
+## Technical Concerns
+- `docs/CONCERNS.md`
+
+## Coding Conventions
+- `docs/CONVENTIONS.md`
+
+## Integrations
+- `docs/INTEGRATIONS.md`
+
+## Technology Stack
+- `docs/STACK.md`
+
+## Project Structure
+- `docs/STRUCTURE.md`
+
+## Testing Constraints
+- `docs/TESTING.md`
+
+---
+
+# Database Source of Truth
+
+Database documentation under `docs/db_structure/` is authoritative.
+
+## Core Schema
+- `docs/db_structure/database_schema_documentation.md`
+
+## Stored Procedures
+- `docs/db_structure/stored_procedures/*.sql`
+
+## Functions
+- `docs/db_structure/functions/*.sql`
+
+## Views
+- `docs/db_structure/views/*.sql`
+
+## User-defined Data Types
+- `docs/db_structure/data_types/*.sql`
+
+Claude MUST:
+- verify schema before generating SQL
+- verify joins before writing queries
+- verify procedures/functions/views before introducing new SQL
+- avoid hallucinating schema objects
+- prefer existing DB patterns
+- preserve actual SQL types and nullability
+
+---
+
+# Required Workflow
+
+For any non-trivial task:
+
+1. Identify affected subsystems
+2. Identify relevant documentation
+3. Locate similar implementations
+4. Understand existing patterns
+5. Evaluate runtime implications
+6. Produce a short implementation plan
+7. Then implement
+
+---
+
+# Architectural Rules
+
+## Dependency Injection
+
+New code should use constructor injection.
+
+Avoid expanding legacy static service locator patterns unless compatibility requires it.
+
+## Request Handlers
+
+Client commands must follow the existing handler architecture:
+- command registration in `Commands.cs`
+- handler in `Perpetuum.RequestHandlers`
+- Autofac registration
+
+Handlers should remain thin orchestration layers.
+
+Business logic belongs in services/domain systems.
+
+## Zone Safety
+
+Respect the single `ProcessManager` loop architecture.
+
+Avoid:
+- blocking operations inside zone updates
+- `.Result` / synchronous task waits
+- long synchronous DB operations in hot paths
+- unsafe shared-state mutation
+
+## Database Access
+
+Prefer existing subsystem patterns.
+
+Use repositories where they already exist.
+
+Avoid:
+- unsafe SQL interpolation
+- `SELECT *`
+- duplicated SQL logic
+- schema assumptions
+
+## Error Handling
+
+Use existing patterns:
+- `PerpetuumException`
+- `ErrorCodes`
+- `ThrowIf*` guard extensions
+
+---
+
+# Technical Debt Rules
+
+Avoid worsening known technical debt documented in:
+- `docs/CONCERNS.md`
+
+Avoid:
+- new static service locators
+- new magic constants
+- unsafe SQL patterns
+- fire-and-forget async without cancellation
+- new `#if DEBUG` behavioral divergence
+
+---
+
+# Modification Rules
+
+When modifying existing systems:
+- preserve public contracts
+- preserve serialization compatibility
+- preserve DB compatibility
+- preserve network protocol compatibility
+- preserve threading assumptions
+- preserve runtime behavior
+
+Avoid broad refactors unless explicitly requested.
+
+---
+
+# Performance Rules
+
+Evaluate runtime impact before introducing:
+- LINQ in hot paths
+- blocking waits
+- excessive allocations
+- immutable collection churn
+- synchronous DB work in update loops
+
+High-risk hot paths include:
+- zone updates
+- NPC AI
+- combat
+- movement
+- market processing
+- season activity tracking
+
+---
+
+# Security Rules
+
+Never:
+- introduce plaintext credentials
+- weaken authentication
+- bypass access validation
+- introduce unsafe SQL construction
+
+Prefer:
+- parameterized queries
+- existing auth flows
+- existing validation patterns
+
+---
+
+# Testing & Validation
+
+There is currently no automated test suite.
+
+Claude MUST:
+- propose manual validation steps
+- identify affected gameplay systems
+- identify affected DB state
+- identify likely regression areas
+
+---
+
+# Response Expectations
+
+For implementation tasks, provide:
+
+1. Affected systems
+2. Relevant files/docs consulted
+3. Risks and constraints
+4. Implementation plan
+5. Code changes
+6. Manual validation steps
+7. Potential regressions
+
+For DB-related tasks:
+- mention consulted tables/views/procedures/functions
+- explain important relationship paths when relevant
+
+Avoid generating code before analysis.
+
+---
+
+# Code Placement
+
+Before creating files:
+- verify correct subsystem placement in `docs/STRUCTURE.md`
+- follow existing namespace patterns
+- follow existing folder organization
+
+Avoid parallel abstractions unless justified.
+
+---
+
+# AI Contribution Rules
+
+## Where to Edit
+
+| Purpose | File |
+|---|---|
+| Main AI instructions | `CLAUDE.md` |
+| Architecture deep-dive | `.claude/knowledge/architecture.md` |
+| Specialist agents | `.claude/agents/.md` |
+
+
+---
+
+# Backlog Management
+
+Persistent project backlog files are authoritative project memory.
+
+## Backlog Files
+
+Primary:
+- `docs/backlog/issues.md`
+- `docs/backlog/improvements.md`
+
+Optional:
+- `docs/backlog/active-sprint.md`
+- `docs/backlog/completed.md`
+
+## Backlog Rules
+
+Claude MUST:
+- review backlog files before major implementation work
+- avoid duplicate backlog entries
+- update related backlog items after implementation
+- preserve backlog structure and identifiers
+- prefer updating existing items over creating duplicates
+- keep backlog entries concise and structured
+- move completed items to `completed.md` when appropriate
+
+When asked to:
+- "work on backlog"
+- "pick a task"
+- "continue work"
+- "fix issues"
+- "implement improvements"
+
+Claude should:
+1. review backlog files
+2. prioritize unfinished HIGH priority items
+3. prefer low-risk/high-impact work unless instructed otherwise
+4. produce a short implementation plan
+5. update backlog status after work completes
+
+## Backlog Statuses
+
+Use:
+- TODO
+- IN_PROGRESS
+- BLOCKED
+- DONE
+- DEFERRED
+
+## Backlog Priorities
+
+Use:
+- CRITICAL
+- HIGH
+- MEDIUM
+- LOW
+
+## Recommended Backlog Entry Format
+
+```md
+## ISSUE-001 - Short title
+
+Status: TODO
+Priority: HIGH
+Area: Networking
+
+### Problem
+Concise issue description.
+
+### Impact
+Runtime/gameplay/maintenance impact.
+
+### Proposed Fix
+Short implementation direction.
+
+### Notes
+Optional additional context.
+```
+
diff --git a/docs/Patches/p36.0/Features/Seasons/migration.sql b/docs/Patches/p36.0/Features/Seasons/migration.sql
new file mode 100644
index 0000000..8427ecb
--- /dev/null
+++ b/docs/Patches/p36.0/Features/Seasons/migration.sql
@@ -0,0 +1,86 @@
+-- Seasons System Migration
+-- Run once against the game database before deploying the updated server binary.
+
+CREATE TABLE seasons (
+ id INT IDENTITY(1,1) NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ description VARCHAR(512) NOT NULL DEFAULT '',
+ start_time DATETIME NOT NULL,
+ end_time DATETIME NOT NULL,
+ is_active BIT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_seasons PRIMARY KEY (id)
+);
+
+CREATE TABLE season_activity_rates (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ activity_type INT NOT NULL,
+ points_per_unit FLOAT NOT NULL,
+ unit_scale INT NOT NULL DEFAULT 1,
+ CONSTRAINT PK_season_activity_rates PRIMARY KEY (id)
+);
+
+CREATE TABLE season_objectives (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ name VARCHAR(128) NOT NULL,
+ description VARCHAR(512) NOT NULL DEFAULT '',
+ activity_type INT NOT NULL,
+ target_value BIGINT NOT NULL,
+ bonus_points INT NOT NULL,
+ display_order INT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_season_objectives PRIMARY KEY (id)
+);
+
+CREATE TABLE season_tiers (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ tier_number INT NOT NULL,
+ tier_name VARCHAR(64) NOT NULL,
+ points_required INT NOT NULL,
+ package_id INT NOT NULL,
+ CONSTRAINT PK_season_tiers PRIMARY KEY (id)
+);
+
+CREATE TABLE season_leaderboard_rewards (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ rank_min INT NOT NULL,
+ rank_max INT NOT NULL,
+ package_id INT NOT NULL,
+ CONSTRAINT PK_season_leaderboard_rewards PRIMARY KEY (id)
+);
+
+CREATE TABLE season_character_points (
+ character_id INT NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ total_points FLOAT NOT NULL DEFAULT 0,
+ last_updated DATETIME NOT NULL DEFAULT GETUTCDATE(),
+ intro_mail_sent BIT NOT NULL DEFAULT 0,
+ leaderboard_reward_delivered BIT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_season_character_points PRIMARY KEY (character_id, season_id)
+);
+
+CREATE TABLE season_objective_progress (
+ character_id INT NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ objective_id INT NOT NULL REFERENCES season_objectives(id),
+ current_value FLOAT NOT NULL DEFAULT 0,
+ completed BIT NOT NULL DEFAULT 0,
+ completed_time DATETIME NULL,
+ bonus_awarded BIT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_season_objective_progress PRIMARY KEY (character_id, season_id, objective_id)
+);
+
+CREATE TABLE season_tier_claims (
+ character_id INT NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ tier_id INT NOT NULL REFERENCES season_tiers(id),
+ claimed_time DATETIME NOT NULL DEFAULT GETUTCDATE(),
+ CONSTRAINT PK_season_tier_claims PRIMARY KEY (character_id, season_id, tier_id)
+);
+
+-- Indexes for common query patterns
+CREATE INDEX IX_season_character_points_season ON season_character_points (season_id, total_points DESC);
+CREATE INDEX IX_season_objective_progress_char ON season_objective_progress (character_id, season_id);
+CREATE INDEX IX_season_tier_claims_char ON season_tier_claims (character_id, season_id);
diff --git a/docs/ROBOT_TEMPLATE_SAMPLE.md b/docs/ROBOT_TEMPLATE_SAMPLE.md
new file mode 100644
index 0000000..2332571
--- /dev/null
+++ b/docs/ROBOT_TEMPLATE_SAMPLE.md
@@ -0,0 +1,22 @@
+# Sample formats
+
+## Template Genxy string format
+
+#robot=i158C#head=i158D#chassis=i158E#leg=i158F#container=i14A#headModules=[|m0=[|definition=i302|slot=i1]|m1=[|definition=i302|slot=i2]|m2=[|definition=i314|slot=i3]|m3=[|definition=i3A7|slot=i4]|m4=[|definition=i3A7|slot=i5]]#chassisModules=[|m0=[|definition=i34D|slot=i1|ammoDefinition=i986|ammoQuantity=i17]|m1=[|definition=i34D|slot=i2|ammoDefinition=i986|ammoQuantity=i17]|m2=[|definition=i34D|slot=i3|ammoDefinition=i986|ammoQuantity=i17]|m3=[|definition=i34D|slot=i4|ammoDefinition=i986|ammoQuantity=i17]]#legModules=[|m0=[|definition=i2BA|slot=i1]|m1=[|definition=i33B|slot=i2|ammoDefinition=i298|ammoQuantity=i8]|m2=[|definition=i3BC|slot=i2]|m3=[|definition=i3BC|slot=i4]|m4=[|definition=i2B1|slot=i5]]
+
+## Robot part options format
+
+#height=f0.45#slotFlags=4451,6d1,451,6d3
+
+# Description
+
+- Robot consists of 4 parts: head, chassis, leg, container. Each part has a definition (e.g. i158C) and a set of modules. Each module has a definition, a slot number, and optionally ammo definition+quantity.
+- The Genxy string encodes all of this in a compact form. The editor will decode it into a structured form for editing, then re-encode on save.
+- The editor will also show the translated names for each definition using the `TranslationService` from Phase 2, so you see human-readable labels instead of just "i158C".
+- The editor will allow adding/removing modules, changing definitions, and editing ammo quantities. Each change will produce an `IPendingChange` that can be applied directly to the DB or exported as SQL.
+- The loot editor will show the items contained in a robot's container, allowing you to add/remove items and change quantities. This also produces `IPendingChange` entries for DB/script.
+- This template is just a sample to illustrate the format. The actual editor will have a more user-friendly UI with dropdowns for definitions, drag-and-drop module assignment, and real-time translation lookups.
+- The goal is to make it easy for admins to create complex robot configurations without having to manually write Genxy strings or SQL statements. The tool will handle all the encoding/decoding and provide a visual interface for managing robot templates and their loot.
+- The editor will also validate changes before applying them, ensuring that module definitions are compatible with their assigned slots and that ammo quantities are within allowed limits. This helps prevent errors and ensures that the resulting robot configurations are valid within the game's rules.
+- Overall, this tool will streamline the process of managing robot templates and loot tables, making it more efficient and less error-prone for administrators to maintain the game world.
+- Robot part options format is a compact way to encode the height and slot flags for a robot part. The height is a floating-point value representing the part's height, while the slot flags are a comma-separated list of hexadecimal values representing the available slots for modules on that part. This format allows for easy parsing and editing of robot part configurations within the admin tool.
\ No newline at end of file
diff --git a/docs/backlog/active-sprint.md b/docs/backlog/active-sprint.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/backlog/completed.md b/docs/backlog/completed.md
new file mode 100644
index 0000000..e69de29
diff --git a/docs/backlog/improvements.md b/docs/backlog/improvements.md
new file mode 100644
index 0000000..77ce4bd
--- /dev/null
+++ b/docs/backlog/improvements.md
@@ -0,0 +1,255 @@
+## IMPROVEMENT-001 - Recurring Seasons with Selectable Periodicity
+
+Status: TODO
+Priority: HIGH
+Area: Seasons
+
+### Description
+Add the ability to mark a Season as recurring, with a configurable periodicity (e.g. weekly, monthly, custom interval). A recurring Season should auto-start at its `date_start` and automatically schedule the next iteration upon completion, without manual admin intervention.
+
+### Impact
+Reduces operational overhead for regular competitive seasons. Enables a predictable cadence for players and removes the need to manually create and activate each season cycle.
+
+### Proposed Implementation
+- Add `is_recurring` (bit) and `recurrence_period_days` (int, nullable) columns to the `seasons` table.
+- On season end, the server-side season scheduler checks `is_recurring`; if true, clones the season with `date_start = previous date_end` and `date_end = date_start + recurrence_period_days`, then activates it.
+- Auto-start logic: the existing season scheduler (or a new timed check) compares `date_start` against `DateTime.UtcNow` and activates eligible recurring seasons automatically.
+- Admin tool should expose `is_recurring` and `recurrence_period_days` fields when creating or editing a season.
+- Ensure the recurrence chain is bounded (e.g. optional `recurrence_end_date` or max iteration count) to prevent unbounded DB growth.
+
+### Notes
+Depends on [[ISSUE-001]] — UTC enforcement on `date_start`/`date_end` must be in place before auto-start timing is reliable.
+Periodicity options to support at minimum: daily, weekly, biweekly, monthly, custom (n days).
+
+## IMPROVEMENT-002 - Refactor Hardcoded System Characters and Channels
+
+Status: TODO
+Priority: HIGH
+Area: Chat / Seasons / Infrastructure
+
+### Description
+System characters (e.g. `[OPP] Announcer`) and system channels (e.g. `Seasons Info`) are currently referenced by hardcoded name strings scattered across the codebase. These should be centralised and driven by configuration or well-defined constants so they can be changed without touching multiple call sites.
+
+### Impact
+Hardcoded strings are fragile: a rename or new deployment environment requires hunting down every occurrence. Centralising them reduces maintenance cost, eliminates copy-paste errors, and makes the system easier to extend (e.g. adding a new announcement channel for a different feature).
+
+### Proposed Implementation
+- Audit the codebase for all string literals that reference system character names and channel names.
+- Introduce a `SystemCharacters` static class (or config-backed equivalent) with named constants / properties for each system character (e.g. `SystemCharacters.Announcer`).
+- Introduce a `SystemChannels` static class (or config-backed equivalent) with named constants / properties for each system channel (e.g. `SystemChannels.SeasonsInfo`).
+- Replace all hardcoded occurrences with references to these constants.
+- Where values should be operator-configurable (e.g. different server deployments), back them with `gameConfig` or a dedicated config section rather than compile-time constants.
+- Update the Admin Tool if it surfaces any of these names directly.
+
+### Notes
+Audit starting points: seasons announcement code, chat subsystem, any admin tool chat/broadcast helpers.
+Keep backward compatibility with existing DB channel records — constants should match stored names unless a migration is also performed.
+
+## IMPROVEMENT-003 - Admin Tool: Item Designer
+
+Status: TODO
+Priority: MEDIUM
+Area: Admin Tool / Items
+
+### Description
+Add an Item Designer feature to the Admin Tool that allows operators to create new game items from scratch. The designer should cover basic item parameters, configurable item stats, a side-by-side comparison view against an existing item, and translation entry for all supported locales.
+
+### Impact
+Currently creating new items requires direct DB manipulation and knowledge of multiple interrelated tables. A guided UI reduces the risk of malformed items, lowers the barrier for content authors, and speeds up content iteration.
+
+### Proposed Implementation
+- **Basic Parameters panel** — item name (internal key), category, type, volume, mass, tier, icon, flags (marketable, stackable, etc.).
+- **Stats panel** — dynamic list of stat key/value pairs drawn from the known `entitydefaults` / `aggregatevalues` schema; support adding, editing, and removing stat rows with type validation.
+- **Comparison panel** — item picker to load an existing item alongside the new item; display both sets of parameters and stats in a diff-style view so the designer can use an existing item as a reference template.
+- **Translations panel** — entry fields for item display name and description per supported locale; pre-populate from the selected reference item if one is chosen.
+- **Save flow** — validate required fields, then write to the relevant tables (`entitydefaults`, `aggregatevalues`, `translation`, etc.) in a single transaction; report success or validation errors inline.
+- Consider a "Clone from existing" shortcut that pre-fills all panels from a chosen item, reducing the common case of creating a variant.
+
+### Notes
+Requires understanding of the full item definition schema — consult `docs/db_structure/` before implementation.
+Translation keys must follow existing naming conventions to avoid collisions.
+The comparison/reference view is a UX aid only; it must not overwrite the new item's data silently.
+
+## IMPROVEMENT-004 - Admin Tool: Robot Designer
+
+Status: TODO
+Priority: MEDIUM
+Area: Admin Tool / Robots
+
+### Description
+Add a Robot Designer feature to the Admin Tool that allows operators to create new robots from scratch. The designer covers selecting a robot template, configuring basic robot parameters, setting stats for the robot chassis and each robot part (head, leg, chassis, inventory), a side-by-side comparison view against an existing robot, and translation entry for all supported locales.
+
+### Impact
+Creating new robots currently requires direct manipulation of multiple interrelated DB tables (robot definition, parts, slots, stats, translations). A guided UI reduces the risk of malformed robot definitions, lowers the barrier for content authors, and speeds up robot content iteration — especially important given robot complexity relative to generic items.
+
+### Proposed Implementation
+- **Template panel** — select a robot template (chassis archetype) that pre-defines the part layout (number of head/leg/chassis/inventory slots, turret/missile/aux slot counts). Templates should be drawn from existing robot definitions.
+- **Basic Parameters panel** — robot name (internal key), faction, tier, icon, size class, flags (marketable, constructable, etc.).
+- **Robot Stats panel** — stats applied to the robot entity itself (speed, sensor, accumulator, etc.), drawn from the known `aggregatevalues` schema for robot entities.
+- **Parts Stats panel** — per-part (head, leg, chassis, inventory) stat configuration; each part is a separate sub-entity with its own `entitydefaults` / `aggregatevalues` rows. Support adding, editing, and removing stat rows per part with type validation.
+- **Comparison panel** — robot picker to load an existing robot alongside the new definition; display chassis + all parts parameters and stats in a diff-style view for reference. Must not auto-apply reference values to the new robot.
+- **Translations panel** — display name and description per supported locale for the robot and each named part; pre-populate from the selected reference robot if one is chosen.
+- **Save flow** — validate all required fields across robot and parts, then write the full robot definition (robot entity, part entities, slot assignments, stats, translations) in a single transaction; report success or validation errors inline.
+- Consider a "Clone from existing robot" shortcut that pre-fills all panels from a chosen robot, covering the common variant/reskin workflow.
+
+### Notes
+Robot definitions span multiple tables — consult `docs/db_structure/` thoroughly before implementation; pay attention to part ownership and slot assignment relationships.
+Translation keys for robot and parts must follow existing naming conventions.
+The template selection step is critical: slot counts and part types are structurally fixed by the template and must not be violated by subsequent panel edits.
+See [[IMPROVEMENT-003]] for the related Item Designer — shared UI patterns (stats panel, translations panel, comparison panel) should be extracted as reusable components.
+
+## IMPROVEMENT-005 - Seasons: Additional Activity Types
+
+Status: TODO
+Priority: MEDIUM
+Area: Seasons / Activities
+
+### Description
+Expand the Seasons activity tracking system with new activity types beyond the current set. Candidate types include: production runs, artifacting, module or deployable usage, and island visitation. Each new type should integrate with the existing scoring and point-accumulation pipeline.
+
+### Impact
+A broader set of tracked activities makes seasons more engaging for a wider range of playstyles (industrialists, explorers, etc.), not just combat-focused players. It also provides more levers for season designers to tune the competitive balance of each season's objectives.
+
+### Proposed Implementation
+- **Production** — award points when a production job completes; parameterisable by item category, tier, or quantity produced.
+- **Artifacting** — award points on successful artifact scan/loot events; parameterisable by artifact tier or island type.
+- **Module / Deployable Usage** — award points when a specific module type or deployable is activated/deployed; parameterisable by module category or deployable type.
+- **Island Visitation** — award points the first time (or each time, configurable) a character enters a specific island or island category (alpha/beta/gamma) within a season.
+- Each new activity type should follow the existing activity handler pattern: a discrete handler class, registration in the activity type registry, and a corresponding `season_activity_types` DB record.
+- Point values and activity parameters should remain data-driven (DB/config) rather than hardcoded, consistent with existing activity types.
+- Ensure anti-farming guards (cooldowns, per-session caps) can be configured per activity type, consistent with [[IMPROVEMENT-001]] recurring season design.
+
+### Notes
+Audit existing activity tracking hooks in the production, scanning, module, and zone subsystems before wiring new event sources — prefer tapping existing domain events over introducing new ones.
+Island visitation tracking must be zone-thread-safe; consult zone update loop constraints in `docs/CONCERNS.md`.
+Anti-farming considerations are especially important for high-frequency events (module usage, production) — caps must be configurable at the season level.
+
+## IMPROVEMENT-006 - Daily Objectives
+
+Status: TODO
+Priority: MEDIUM
+Area: Seasons / Objectives
+
+### Description
+Introduce daily objectives: a set of objectives that reset and re-issue automatically every day. The system should reuse or extend existing objective infrastructure wherever possible, adding only the recurrence scheduling layer on top.
+
+### Impact
+Daily objectives provide a regular engagement loop that encourages players to log in consistently, broadening the appeal of the seasons system beyond one-time or long-horizon goals.
+
+### Proposed Implementation
+- Audit existing objective types, completion tracking, and reward pipeline — identify what can be reused verbatim vs. what needs extension.
+- Add a `recurrence` flag (or subtype) to the objective definition that marks an objective as daily-recurring.
+- Implement a daily reset scheduler: at UTC midnight (or a configurable daily reset time), mark all completed daily objectives as eligible for re-issue and create new completion records for the new day.
+- Per-character completion state must be scoped to the current day's window so prior-day completions do not block re-issuance.
+- Daily objectives should be configurable per season: which objective types appear, their targets, and their point/reward values.
+- Ensure the reset scheduler is idempotent — a server restart mid-day must not re-issue objectives already issued for that day.
+
+### Notes
+Depends on [[ISSUE-001]] — daily reset boundary must use UTC to be consistent across deployments.
+See [[IMPROVEMENT-005]] for new activity types that could back daily objective targets.
+See [[IMPROVEMENT-001]] for recurring season design — daily objectives are a finer-grained recurrence within a season.
+Reset time should be operator-configurable (default UTC midnight) rather than hardcoded.
+
+## IMPROVEMENT-007 - NPC Rank System
+
+Status: TODO
+Priority: LOW
+Area: NPCs
+
+### Description
+Add a manually assigned rank field to NPC definitions so that NPCs can be categorised and distinguished by rank level (e.g. grunt, elite, commander, boss). Rank should be a lightweight data attribute — no automated assignment logic.
+
+### Impact
+Provides a clear, queryable signal for distinguishing NPC threat levels without relying on inferred stats or naming conventions. Useful for display, loot table differentiation, season activity targeting (e.g. "kill 5 elite NPCs"), and future AI behaviour tuning.
+
+### Proposed Implementation
+- Add a `rank` column (tinyint or small enum-backed int, nullable) to the NPC definition table; `NULL` means unranked.
+- Define a fixed rank scale (e.g. 0 = Minion, 1 = Standard, 2 = Elite, 3 = Commander, 4 = Boss) as named constants in code — consult existing NPC categorisation patterns before finalising values.
+- Rank is assigned manually via the Admin Tool or direct DB edit; no automated inference.
+- Expose rank in the NPC definition read path so it is available to callers (loot, season activity handlers, UI).
+- Admin Tool: surface the rank field in the NPC editor as a dropdown.
+
+### Notes
+Keep the rank scale small and stable — it will be referenced by season activity configs and potentially loot rules, so changes after rollout are costly.
+If season activity types need to filter by NPC rank (see [[IMPROVEMENT-005]]), the rank value must be accessible at the point where kill events are emitted.
+
+## IMPROVEMENT-008 - NPC Role System
+
+Status: TODO
+Priority: LOW
+Area: NPCs / AI
+
+### Description
+Add a role field to NPC definitions to classify each NPC by its intended combat function (e.g. Combat, Ewar, Support). Roles are assigned manually and serve as a semantic tag for AI behaviour selection, season activity filtering, and general NPC distinction.
+
+### Impact
+Role classification gives AI subsystems and content systems a stable, queryable signal for NPC function without relying on module loadout inference or naming conventions. Enables future AI improvements (e.g. role-aware targeting, formation logic) and allows season objectives to target specific NPC roles (e.g. "neutralise 3 Ewar NPCs").
+
+### Proposed Implementation
+- Add a `role` column (tinyint or small enum-backed int, nullable) to the NPC definition table; `NULL` means no role assigned.
+- Define an initial role set as named constants: Combat, Ewar, Support — keep the set open to extension but stable at the value level.
+- Role is assigned manually via Admin Tool or direct DB edit; no automated inference.
+- Expose role in the NPC definition read path so it is available to AI handlers, loot logic, and season activity handlers.
+- Admin Tool: surface the role field in the NPC editor as a dropdown alongside the rank field (see [[IMPROVEMENT-007]]).
+- AI subsystem: role is available as a hint for future behaviour selection — no behavioural changes required in this improvement, just the data plumbing.
+
+### Notes
+Role and rank (see [[IMPROVEMENT-007]]) are complementary attributes — implement consistently (same table, same read path, same Admin Tool panel).
+If season activity types need to filter by NPC role (see [[IMPROVEMENT-005]]), role must be accessible at the point where kill events are emitted.
+Keep the initial role set conservative; adding roles later is cheaper than changing existing ones after downstream systems reference them.
+
+## IMPROVEMENT-009 - Targeted Objectives
+
+Status: TODO
+Priority: LOW
+Area: Seasons / Objectives
+
+### Description
+Extend the objective system to support targeted objectives, where a specific subject must be matched for progress to count. The target is activity-type-dependent — for example, a mining objective can target a specific ore type ("Mine 100 000 Colixium"), a kill objective can target an NPC role ("Kill 50 Combat NPCs") or rank, a production objective can target an item category, and so on.
+
+### Impact
+Targeted objectives allow season designers to create more varied and specific challenges, directing player behaviour toward particular content rather than rewarding any activity of a given type. This significantly increases the design space for seasons and daily objectives.
+
+### Proposed Implementation
+- Extend the objective definition schema with an optional `target_filter` structure: a type-discriminated payload whose shape is determined by the activity type (e.g. `{ type: "item", definition_id: 123 }` for mining, `{ type: "npc_role", role: 1 }` for kills).
+- Each activity handler is responsible for evaluating whether the event matches the objective's target filter before crediting progress; no-filter objectives behave as today (match all).
+- Target filter types to implement initially, aligned with supported activity types:
+ - **Mining** — specific ore `definition_id` or ore category.
+ - **NPC kill** — NPC `role` (see [[IMPROVEMENT-008]]) and/or `rank` (see [[IMPROVEMENT-007]]).
+ - **Production** — item category or specific `definition_id`.
+ - **Artifacting** — artifact tier or island type.
+ - **Island visitation** — specific island or island category (alpha/beta/gamma).
+- Admin Tool: when defining an objective, show a target picker whose options are driven by the selected activity type.
+- Objective display text should incorporate the target name (resolved from `definition_id` or enum label) for readable in-game descriptions.
+
+### Notes
+Depends on [[IMPROVEMENT-005]] for the activity types that targeted objectives will filter against.
+Depends on [[IMPROVEMENT-007]] and [[IMPROVEMENT-008]] for NPC rank/role filtering on kill objectives.
+Target filter should be stored as structured data (e.g. JSON column or normalised filter table) rather than freeform strings to allow reliable matching and Admin Tool rendering.
+Keep the filter evaluation path lightweight — it runs on every matching game event and must not introduce blocking or excessive allocation in hot paths.
+
+## IMPROVEMENT-010 - Seasons Scoring Balancing Tab
+
+Status: TODO
+Priority: LOW
+Area: Seasons / Admin Tool
+
+### Description
+Add a Scoring Balancing tab to the season editor in the Admin Tool. The tab presents a consolidated view of tiers, objectives, activity point rates, and the computed number of activities required per objective — all editable inline — so season designers can tune scoring balance without cross-referencing multiple screens or raw DB rows.
+
+### Impact
+Season balance currently requires manual cross-referencing of tier thresholds, objective point values, and activity rates in separate views or directly in the DB. A unified balancing surface reduces errors, makes trade-offs immediately visible, and significantly speeds up the iteration loop for season design.
+
+### Proposed Implementation
+- **Tiers panel** — list all tiers for the season (name, point threshold, reward); editable inline.
+- **Objectives panel** — list all objectives (name, activity type, target filter if any, point value); editable inline; derived column shows point contribution as a percentage of the next tier threshold.
+- **Activity Rates panel** — list all activity types configured for the season with their point-per-event rate; editable inline.
+- **Activities-to-Objective column** — for each objective, compute and display `objective_target / activity_rate` (i.e. how many raw activity events are needed to complete it at the current rate); updates live as rates or targets are edited.
+- All edits are staged locally and applied in a single save transaction to avoid partial state.
+- Read-only summary row at the bottom: estimated total points available if all objectives are completed, compared against the top tier threshold — flags imbalance if the gap is large.
+
+### Notes
+The activities-to-objective computation is a display convenience; the authoritative values remain the stored point rates and objective targets.
+Depends on [[IMPROVEMENT-005]] for the full set of activity types surfaced in the rates panel.
+Depends on [[IMPROVEMENT-009]] for targeted objectives appearing in the objectives panel with their filter displayed.
+Edits made here must write through the same save paths used by the individual objective and activity rate editors — no parallel write logic.
diff --git a/docs/backlog/issues.md b/docs/backlog/issues.md
new file mode 100644
index 0000000..bb86505
--- /dev/null
+++ b/docs/backlog/issues.md
@@ -0,0 +1,21 @@
+## ISSUE-001 - Enforce UTC for seasons.date_start and seasons.date_end
+
+Status: DONE
+Priority: HIGH
+Area: Seasons / Database
+
+### Problem
+All usages of `seasons.date_start` and `seasons.date_end` must be enforced to UTC. Currently there is no guaranteed UTC enforcement at the read/write boundary, which can cause incorrect season activation windows if server or client time zones differ.
+
+### Impact
+Season start/end boundaries may be evaluated incorrectly under non-UTC system time, causing seasons to activate or expire at wrong times — affecting rewards, eligibility windows, and any logic gated on season date comparisons.
+
+### Proposed Fix
+- Audit all C# code that reads `date_start` / `date_end` from the `seasons` table and ensure `DateTime.SpecifyKind(..., DateTimeKind.Utc)` or `DateTimeOffset` is applied on read.
+- Audit all write paths (INSERT / UPDATE) to ensure values are converted to UTC before persistence.
+- Audit stored procedures and views that reference `seasons.date_start` / `seasons.date_end` for any implicit local-time assumptions.
+- Consider adding a DB constraint or documented convention that these columns are always UTC.
+
+### Notes
+Related columns: `seasons.date_start`, `seasons.date_end`.
+Any `DateTime.Now` comparisons against these values should become `DateTime.UtcNow`.
diff --git a/docs/codebase/ARCHITECTURE.md b/docs/codebase/ARCHITECTURE.md
new file mode 100644
index 0000000..04095cf
--- /dev/null
+++ b/docs/codebase/ARCHITECTURE.md
@@ -0,0 +1,341 @@
+
+# Architecture
+
+**Analysis Date:** 2026-05-11
+
+## System Overview
+
+```text
+┌──────────────────────────────────────────────────────────────────────┐
+│ Client (TCP) │
+└────────────────────────────┬─────────────────────────────────────────┘
+ │ Encrypted TCP (RC4/RSA handshake)
+ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ Relay / Session Layer │
+│ `src/Perpetuum/Services/Sessions/Session.cs` │
+│ `src/Perpetuum/Network/ClientConnection.cs` │
+│ Authn, command dispatch, character selection │
+└────────────┬───────────────────────────────────────┬─────────────────┘
+ │ IRequest │ IZoneRequest
+ ▼ ▼
+┌────────────────────────┐ ┌─────────────────────────────┐
+│ IRequestHandler │ │ IZoneSession / ZoneSession │
+│ (150+ handlers) │ │ `src/Perpetuum/Zones/ │
+│ `src/Perpetuum. │ │ ZoneSession.cs` │
+│ RequestHandlers/` │ └──────────────┬──────────────┘
+└────────────────────────┘ │
+ │ │
+ ▼ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ Zone (parallel simulation processes) │
+│ `src/Perpetuum/Zones/Zone.cs` (abstract) │
+│ PveZone / PvpZone / StrongHoldZone / TrainingZone │
+│ Each zone: own TCP listener port, terrain grid, units, NPC presences│
+└──────────────┬───────────────────────────────────────────────────────┘
+ │
+ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ Core Game Services (singletons, process-ticked) │
+│ MarketEngine · MissionEngine · ProductionEngine · Seasons │
+│ Standing · Social · EventServices · ExtensionService · Sessions │
+│ `src/Perpetuum/Services/` │
+└──────────────┬───────────────────────────────────────────────────────┘
+ │
+ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ SQL Server (via Db static factory, System.Transactions) │
+│ `src/Perpetuum/Data/Db.cs` │
+└──────────────────────────────────────────────────────────────────────┘
+```
+
+## Component Responsibilities
+
+| Component | Responsibility | Key File |
+|-----------|----------------|----------|
+| `Session` | Per-client TCP connection, auth, command routing | `src/Perpetuum/Services/Sessions/Session.cs` |
+| `IRequestHandler` | Handles a single named command | `src/Perpetuum/Host/Requests/IRequestHandler.cs` |
+| `Command` | Metadata for a command (name, access level, argument schema) | `src/Perpetuum/Command.cs` |
+| `Commands` | Static registry of all ~200+ commands | `src/Perpetuum/Commands.cs` |
+| `Zone` | Self-contained world simulation (process loop) | `src/Perpetuum/Zones/Zone.cs` |
+| `ZoneManager` | Lookup/iterate across all running zones | `src/Perpetuum/Zones/ZoneManager.cs` |
+| `ZoneSession` | Per-player connection within a zone | `src/Perpetuum/Zones/ZoneSession.cs` |
+| `Entity` | Base game object with Eid and property bag | `src/Perpetuum/EntityFramework/Entity.cs` |
+| `EntityDefault` | Static template/definition data (DB-loaded) | `src/Perpetuum/EntityFramework/EntityDefault.cs` |
+| `Unit` | Entity with in-zone physics: armor, core, position, locking | `src/Perpetuum/Units/Unit.cs` |
+| `Robot` | Unit that can equip modules; manages overheat | `src/Perpetuum/Robots/Robot.cs` |
+| `Player` | Robot controlled by a connected character | `src/Perpetuum/Players/Player.cs` |
+| `Module` | Equipment slot on a robot component | `src/Perpetuum/Modules/Module.cs` |
+| `ActiveModule` | Module with a state machine (Idle/Oneshot/AutoRepeat/…) | `src/Perpetuum/Modules/ActiveModule.States.cs` |
+| `ProcessManager` | Ticks all registered `IProcess` instances at ~50 ms | `src/Perpetuum/Threading/Process/ProcessManager.cs` |
+| `HostStateService` | Tracks server lifecycle state (Off/Init/Starting/Online/Stopping) | `src/Perpetuum/Host/HostStateService.cs` |
+| `Db` | Static fluent query factory backed by SQL Server | `src/Perpetuum/Data/Db.cs` |
+
+## Key Patterns
+
+### Dependency Injection (Autofac)
+All services, handlers, and runtime objects are wired in `src/Perpetuum.Bootstrapper/`.
+Each major subsystem has its own Autofac `Module` in `src/Perpetuum.Bootstrapper/Modules/`.
+Constructor injection is the exclusive DI style.
+Factory delegates (`Func`, typed delegate aliases) are registered to allow deferred or
+parameterised construction without breaking DI.
+
+Startup order in `PerpetuumBootstrapper.Init()`:
+1. `ContainerBuilder` assembles all modules
+2. `IContainer` built; static singletons wired (`EntityDefault.Reader`, `Entity.Services`, …)
+3. `InitGame()` calls `initServer` stored procedure to clean runtime DB tables
+4. `HostStateService.State = Init → Starting → Online`
+
+### Command / Request Handler Pattern
+
+```
+Client TCP frame
+ → Session.OnDataReceived (GenxyReader deserialises key-value packet)
+ → commandFactory(text) → Command lookup (keyed in Autofac by UPPER text)
+ → RequestHandlerFactory(command) → IRequestHandler
+ → handler.HandleRequest(request)
+```
+
+For zone-scoped commands (player is in a zone):
+```
+ → RequestHandlerFactory(command) → IRequestHandler
+ → handler.HandleRequest(zoneRequest) // zoneRequest.Zone is populated
+```
+
+All handlers live in `src/Perpetuum.RequestHandlers/` (597 .cs files across subdirectories).
+Handlers are registered by `RequestHandlersModule` and `ZoneRequestHandlersModule` in the bootstrapper.
+To add a command: define it in `src/Perpetuum/Commands.cs`, create a handler class,
+register it in the appropriate bootstrapper module.
+
+### Entity System
+
+Inheritance chain (bottom-up):
+```
+Entity (Eid, DynamicProperties, Owner, Parent hierarchy)
+ └─ Item (ItemProperty collection, volume, quantity)
+ └─ Unit (position, armor, core, speed, locking, damage, effects)
+ └─ Robot (module components, overheat, locking handler)
+ ├─ Player (character reference, mission handler, blob)
+ ├─ Npc (AI, flock membership)
+ └─ SmartCreature (CrystalAI behaviour)
+```
+
+`EntityDefault` holds **static definition data** loaded from the DB at startup.
+`Entity.DynamicProperties` holds **mutable runtime state** as a key-value bag.
+`OptionalProperty` / `UnitOptionalProperty` provide typed access to named properties
+on units (`src/Perpetuum/Units/UnitOptionalProperty.cs`).
+`ItemProperty` holds aggregate-field values with modifier chains on items/modules.
+
+### Zone as a Process
+
+`Zone` extends `Threading.Process.Process` (`src/Perpetuum/Threading/Process/Process.cs`).
+`Zone.Update(TimeSpan)` is called by `ProcessManager` every ~50 ms.
+Each zone holds:
+- `ImmutableDictionary` (lock-free updates via `ImmutableInterlocked`)
+- `ImmutableDictionary`
+- `ImmutableHashSet` (connected clients)
+- Its own `TcpListener` on a dedicated port
+- Terrain grid (`ITerrain`) with layered data (altitude, control, blocking, materials)
+- NPC `PresenceManager` (flocks, presences, spawn rules)
+- Zone-local services: beams, weather, decors, environment effects, plant handler
+
+Zone types: `PveZone`, `PvpZone`, `StrongHoldZone`, `TrainingZone`
+(`src/Perpetuum/Zones/PveZone.cs`, `PvpZone.cs`, `StrongHoldZone.cs`, `TrainingZone.cs`)
+
+Zone configurations read from DB at startup via `ZoneConfigurationReader`
+(`src/Perpetuum/Zones/ZoneConfiguration.cs`).
+
+### Module State Machine
+
+`ActiveModule` uses a `StackFSM` (stack-based finite state machine,
+`src/Perpetuum/StateMachines/StackFSM.cs`).
+
+States: `Idle`, `Oneshot`, `AutoRepeat`, `Disabled`, `AmmoLoad`, `Shutdown`
+
+Module hierarchy:
+```
+Module (powergrid/CPU usage, property modifiers)
+ └─ ActiveModule (state machine, ammo, cycling)
+ ├─ WeaponModule (damage, range)
+ ├─ ArmorRepairModule
+ ├─ HarvesterModule / DrillerModule
+ ├─ SensorJammerModule
+ └─ … (40+ concrete types in src/Perpetuum/Modules/)
+```
+
+### Network & Wire Format
+
+- TCP connections use RSA key exchange then RC4 stream cipher
+ (`src/Perpetuum/Network/EncryptedTcpConnection.cs`)
+- Wire format is **GenXY** — a custom text-based key=value protocol
+ (`src/Perpetuum/GenXY/GenxyReader.cs`, `GenxyWriter.cs`)
+- `GenxyConverter` allows registering custom type serialisers
+- `MessageBuilder` / `IMessage` wrap outgoing packets
+ (`src/Perpetuum/MessageBuilder.cs`)
+
+### Data Access
+
+`Db.Query()` returns a fluent `DbQuery` builder backed by `SqlConnection`.
+All DB calls go through `System.Transactions.TransactionScope` (ReadCommitted).
+`Transaction.Current.OnCommited(...)` is used extensively for post-commit side effects
+(e.g., firing domain events after a successful DB write).
+No ORM; raw SQL or stored procedure calls only.
+
+## Domain Model
+
+```
+Account ──< Character ──< Session ──> Zone
+ │
+ └──> Robot (Player) ──< RobotComponent ──< Module
+ └─ LockHandler ──< Lock
+ └─ EffectHandler ──< Effect
+
+Zone ──< Unit (Npc, PBS objects, gates, relics, eggs …)
+ ──> ITerrain (AltitudeLayer, ControlLayer, BlockingLayer, Materials)
+ ──> PresenceManager ──< Presence ──< Flock ──< Npc
+
+Corporation ──< Character
+Gang ──< Character (transient, in-session)
+```
+
+## Data Flow
+
+### Sign-in and Character Select
+
+1. Client connects → `Session` created (`src/Perpetuum/Services/Sessions/Session.cs`)
+2. RSA key exchange, RC4 established
+3. `signIn` command → `SignIn` handler → authenticates account, sets `AccessLevel`
+4. `characterSelect` command → `CharacterSelect` handler → `session.SelectCharacter(character)`
+5. Client sends `zoneEnter` → zone's `TcpListener` receives new connection
+6. `ZoneSession` created for that zone; player's `Robot` spawned via `ZoneEnterQueueService`
+
+### In-Zone Request
+
+1. Client sends GenXY frame on the zone socket
+2. `ZoneSession.OnDataReceived` deserialises packet
+3. `RequestHandlerFactory` resolves handler by command text
+4. Handler executes, may mutate unit state or DB, sends reply via `session.SendMessage`
+
+### Zone Tick
+
+1. `ProcessManager` fires `zone.Update(elapsed)` every ~50 ms
+2. Zone updates sessions, units, NPC presences, terrain change notifications
+3. `Unit.Update` runs module states, locks, movement, effect ticks
+4. Changed unit properties trigger update packets broadcast to nearby players
+
+## Concurrency Model
+
+- **Single process manager thread** ticks all registered `IProcess` instances at ~50 ms intervals
+ (`src/Perpetuum/Threading/Process/ProcessManager.cs`).
+- **Each zone runs as an `IProcess`** in that single process loop (no per-zone dedicated thread).
+ Zone state mutation (units/sessions dictionaries) uses `ImmutableInterlocked` for lock-free
+ reads from other threads.
+- **Network I/O** is async (socket `BeginReceive`/`BeginSend`); callbacks marshal into the
+ process manager's execution context.
+- **Database transactions** use `System.Transactions.TransactionScope` (ReadCommitted).
+ `Transaction.Current.OnCommited(...)` extension method defers side effects until commit.
+- **No Task/async-await** in core game logic; async is isolated to network receive loops and
+ the `AsyncProcess` wrapper (`src/Perpetuum/Threading/Process/AsyncProcess.cs`).
+
+## Key Subsystems
+
+**Zones** (`src/Perpetuum/Zones/`)
+86 subdirectories covering: NPC system (`NpcSystem/`), player-built structures (`PBS/`),
+intrusion mechanics (`Intrusion/`), terrain (`Terrains/`), combat effects (`Effects/`),
+locking (`Locking/`), teleporting (`Teleporting/`), scanning, mining, harvesting, gates,
+rifts, relics, blobs, proximity devices, training.
+
+**NPC System** (`src/Perpetuum/Zones/NpcSystem/`)
+AI behaviour via CrystalAI (`CrystalAI/`) and custom AI (`AI/`). NPCs organised into
+`Flocks` within `Presences`. Multiple presence types: static, expiring, growing,
+interzone, random. Reinforcement and SAP attacker subsystems.
+
+**PBS (Player-Built Structures)** (`src/Perpetuum/Zones/PBS/`)
+Docking bases, control towers, turrets, reactors, energy wells, production nodes,
+armor repairers, highway nodes, effect nodes — all as `PBSObject` entities.
+
+**Market Engine** (`src/Perpetuum/Services/MarketEngine/`)
+Order book (`MarketOrder`), price collection, auto-orders, robot price writer,
+cleanup service. Initialised after zones load (`MarketHelper.Init()`).
+
+**Mission Engine** (`src/Perpetuum/Services/MissionEngine/`)
+Mission definitions, spots, in-progress tracking, targets, rewards, bonus objects,
+transport assignments. Data cached at startup via `MissionDataCache`.
+
+**Production Engine** (`src/Perpetuum/Services/ProductionEngine/`)
+Crafting, research, calibration, reprocessing. `ProductionManager` ticks active lines.
+
+**Seasons** (`src/Perpetuum/Services/Seasons/`)
+Season lifecycle (active/inactive), activity bonuses, repository, cache.
+
+**Event Services** (`src/Perpetuum/Services/EventServices/`)
+Pub/sub event bus (`EventListenerService`). NPC spawn events, environmental effects,
+EP bonus events dispatched through it.
+
+**Standing** (`src/Perpetuum/Services/Standing/`)
+Faction and corporation standing lookup, cached, updated on mission completion/combat.
+
+**Sessions** (`src/Perpetuum/Services/Sessions/`)
+`SessionManager` — tracks all active `ISession` instances; exposes character-deselect events
+consumed by zones to clean up zone sessions.
+
+## Architectural Constraints
+
+- **Threading:** Single `ProcessManager` loop at ~50 ms. Zone logic runs in that loop.
+ Network callbacks are async and lock-free (immutable collections + `ImmutableInterlocked`).
+- **Global state:** Several static service locators set during bootstrap:
+ `Entity.Services` (`EntityFramework/Entity.cs`), `EntityDefault.Reader`, `Db.DbQueryFactory`,
+ `Character.CharacterFactory`, `MissionHelper`, `PBSHelper`, `Message.MessageBuilderFactory`.
+ These are write-once at startup, read-only thereafter.
+- **Platform:** `[SupportedOSPlatform("windows")]` on both `Perpetuum.Server` and
+ `Perpetuum.Bootstrapper` assemblies — Windows-only due to native dependencies.
+- **No automated tests:** The project has no test projects. Validation is manual or via
+ the `Perpetuum.AdminTool` WPF application.
+- **SQL Server only:** `Microsoft.Data.SqlClient` is hard-wired; no abstraction layer.
+
+## Anti-Patterns
+
+### Static Service Locators
+
+**What happens:** `Entity.Services`, `Db.DbQueryFactory`, `MissionHelper`, `PBSHelper`,
+`Character.CharacterFactory`, and several others are static properties set during
+`PerpetuumBootstrapper.Init()`.
+**Why it's wrong:** Makes unit testing impossible and hides dependencies.
+**Do this instead:** Inject the dependency via constructor. This is the pattern used in
+all newer code (e.g., `SeasonService`, zone-scoped services).
+
+### Property-Injection on Zone
+
+**What happens:** `Zone` properties like `Terrain`, `Beams`, `Weather`, `PresenceManager`
+are public setters wired by Autofac property injection rather than constructor injection.
+**Why it's wrong:** Violates explicit dependencies; object is partially constructed
+between `new` and full property assignment.
+**Do this instead:** Consolidate into constructor parameters or a typed config struct,
+as done in newer subsystems.
+
+## Error Handling
+
+**Strategy:** Synchronous exception propagation with typed `ErrorCodes` enum.
+
+**Patterns:**
+- `.ThrowIfNull(ErrorCodes.X)` extension on nullable results — throw `PerpetuumException`
+- `.ThrowIfZero(ErrorCodes.X)` on `ExecuteNonQuery` results to detect missing rows
+- `PerpetuumException` carries an `ErrorCodes` value sent back to the client as a
+ structured error message (`Message.Builder.WithError(error)`)
+- Unhandled exceptions in process loops are logged via `Logger.Exception(ex)` without
+ crashing the host
+
+## Cross-Cutting Concerns
+
+**Logging:** `Logger` static facade (`src/Perpetuum/Log/`) with pluggable `ILogger` sink.
+Chat events use a separate typed `ILogger`.
+**Validation:** Argument schema declared on `Command.Arguments` list; checked via
+`command.CheckArguments(data)` before handler dispatch.
+**Authentication:** `AccessLevel` enum checked per command. `Session.AccessLevel` is set
+on sign-in and elevated for admin accounts.
+**Transactions:** `Db.CreateTransaction()` wraps all DB mutations in `TransactionScope`.
+Post-commit hooks via `Transaction.Current.OnCommited(...)` (`src/Perpetuum/Data/TransactionExtensions.cs`).
+
+---
+
+*Architecture analysis: 2026-05-11*
diff --git a/docs/codebase/CONCERNS.md b/docs/codebase/CONCERNS.md
new file mode 100644
index 0000000..fbbe8c5
--- /dev/null
+++ b/docs/codebase/CONCERNS.md
@@ -0,0 +1,401 @@
+# Technical Concerns
+
+**Analysis Date:** 2026-05-11
+
+---
+
+## Technical Debt
+
+### Ambient Service Locator (Static Setters) — Widespread
+
+Numerous classes expose services via `public static ... { get; set; }` properties that are injected by the Autofac bootstrapper at startup. This is the service-locator anti-pattern and is pervasive:
+
+- `Db.DbQueryFactory` — `src/Perpetuum/Data/Db.cs:7`
+- `Entity.Services` — `src/Perpetuum/EntityFramework/Entity.cs:10`
+- `EntityDefault.Reader` — `src/Perpetuum/EntityFramework/EntityDefault.cs:10`
+- `Character.CharacterCache`, `Character.CharacterFactory` — `src/Perpetuum/Accounting/Characters/Character.cs:94-96`
+- `Logger.Current` — `src/Perpetuum/Log/Logger.cs:10`
+- `MissionTarget.missionDataCache`, `MissionTarget.ProductionDataAccess` — `src/Perpetuum/Services/MissionEngine/MissionTargets/MissionTarget.cs:125-131`
+- `MissionInProgress.MissionInProgressFactory`, `MissionInProgress.MissionProcessor` — `src/Perpetuum/Services/MissionEngine/Missions/MissionInProgress.cs:79-80`
+- `PBSHelper.ProductionDataAccess`, `PBSHelper.ProductionManager` — `src/Perpetuum/Zones/PBS/PBSHelper.cs:37-38`
+- `SeasonServiceLocator.Instance` — `src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs:5`
+- ~25 more static setters across the codebase
+
+**Impact:** Makes dependency graphs invisible, causes test-unfriendly design, and risks null-reference panics if boot order shifts. The `SeasonServiceLocator` pattern was reintroduced for Seasons rather than injecting `ISeasonService` directly into call sites.
+
+**Fix approach:** Replace with constructor injection where feasible. For cross-cutting concerns (e.g., `Db`, `Logger`), accept the cost but document clearly and avoid propagating the pattern to new code.
+
+---
+
+### Hardcoded Constants and Magic Values
+
+- System character detection by nickname substring: `c.Nick.Contains("[OPP]")` — `src/Perpetuum/Accounting/Characters/Character.cs:76` — a TODO acknowledges this is fragile.
+- Mission beta zone multiplier is a TODO: `//TODO: Beta multiplier!` — `src/Perpetuum/Services/MissionEngine/Missions/MissionVisitor.cs:72`
+- Economy parameters hardcoded in C# rather than in DB: multiple TODOs: `src/Perpetuum/Services/MissionEngine/Missions/MissionInProgress.cs:1477`, `src/Perpetuum/Services/MissionEngine/MissionTargets/MissionTargetRewardCalculator.cs:70`, `src/Perpetuum/Services/MissionEngine/MissionTargets/RandomTargetObjects.cs:390`, `src/Perpetuum/Services/MissionEngine/Missions/RandomMission.cs:67`
+- Mission Alpha zone assumed as neutral: `src/Perpetuum/Services/MissionEngine/Missions/MissionInProgress.cs:406` — marked `//TODO: Fixme`
+- MissionLocation has a known "Syndicatification hack": `src/Perpetuum/Services/MissionEngine/MissionStructures/MissionLocation.cs:221`
+- ZoneEffect player-only flag is hardcoded `true` pending a new DB column: `src/Perpetuum/Zones/Effects/ZoneEffects/ZoneEffectReader.cs:23`
+
+**Impact:** Tuning economy or game balance requires C# changes and redeployment instead of DB updates.
+
+**Fix approach:** Extract economy constants to DB configuration tables; expose them via admin commands.
+
+---
+
+### Excluded LootContainers Directory
+
+The `Perpetuum.csproj` explicitly excludes `Zones\LootContainers\**` from compilation, yet the files exist in source:
+- `src/Perpetuum/Zones/LootContainers/LootContainer.cs`
+- `src/Perpetuum/Zones/LootContainers/FieldContainer.cs` (and 5 other files)
+
+**Impact:** These files are dead code that could cause confusion. Any attempt to use these types will fail silently at build time.
+
+**Fix approach:** Either remove the excluded files entirely or restore the compilation and reconcile with the active looting system in `src/Perpetuum/Services/Looting/`.
+
+---
+
+### SQL Injected via Array Interpolation
+
+`ArrayToString()` produces a comma-separated string of IDs that is interpolated directly into SQL `IN (...)` clauses:
+
+- `src/Perpetuum/Accounting/AccountManager.cs:98,232,278`
+- `src/Perpetuum/Groups/Alliances/AllianceHelper.cs:19`
+
+These are integer IDs (not user text), so the practical injection risk is low, but the pattern is not safe by construction.
+
+**Fix approach:** Use table-valued parameters or parameterized batch constructs instead of string interpolation.
+
+---
+
+### 673 Inline `Db.Query()` Call Sites
+
+All database access is via a global static `Db.Query()` fluent builder with no repository abstraction enforced across most of the codebase. The 673 call sites are scattered from `Character.cs` to `Market.cs` to `Outpost.cs`.
+
+**Impact:** No single place to add query logging, timeout configuration, retry logic, or connection-level instrumentation. Refactoring the data layer is prohibitively expensive.
+
+**Fix approach:** Gradually migrate high-churn subsystems (Seasons, Mission Engine) to repository interfaces. New code should always route through repositories.
+
+---
+
+### 104 Inline `SELECT *` Queries
+
+`select * from ...` is used extensively (104 occurrences). `Database.CreateLookupCache` and `Database.CreateCache` always issue `select * from {table}`.
+
+**Impact:** Over-fetching, fragile mapping when columns are added/removed, and performance cost on large tables.
+
+**Fix approach:** Enumerate explicit columns in queries; update `Database.cs:33,40` helpers to accept a column list.
+
+---
+
+## Architectural Risks
+
+### XOR Stream Cipher as "Encryption"
+
+The `EncryptedTcpConnection` applies a single-byte XOR rolling cipher to all network traffic:
+- `src/Perpetuum/Network/EncryptedTcpConnection.cs`
+
+The cipher uses two hardcoded byte values (`outEncodingByte = 0xCA`, `inDecodingByte = 0xAC`). This is security theater — any network observer can decrypt traffic after seeing a few bytes of known-plaintext.
+
+The per-session RC4 layer added in `ClientConnection` (`src/Perpetuum/Network/ClientConnection.cs`) provides real confidentiality for the admin/test client, but production `ZoneSession` connections do not use this path.
+
+**Impact:** Game traffic between the relay server and zone servers (or between clients and zone ports) is not meaningfully encrypted. Packet sniffing can expose character state and commands.
+
+**Fix approach:** Replace the XOR layer with TLS (`SslStream`) or at minimum per-session AES key exchange using the existing RSA infrastructure.
+
+---
+
+### Passwords Stored and Compared in Plaintext
+
+Account passwords are stored verbatim in the `accounts` table and looked up with a direct SQL equality match:
+- `src/Perpetuum/Accounting/AccountRepository.cs:137-143`
+
+There is no hashing (bcrypt, Argon2, PBKDF2) applied anywhere in the authentication path.
+
+**Impact:** A database breach immediately exposes all user credentials. Credential stuffing is trivially possible.
+
+**Fix approach:** Hash passwords with bcrypt or Argon2 on write; verify on read. Migrate existing rows using a forced-reset flow.
+
+---
+
+### No Rate Limiting or Input Validation at Network Boundary
+
+- No per-IP or per-account connection rate limiting beyond a simple `MaxSessions = 5000` cap.
+- No brute-force protection on the login command.
+- No max packet size enforcement before decryption/decompression (the 8 MB cap in `TcpConnection.cs:12,155` is applied post-length-decode, and a GZip decompress bomb is not guarded against).
+- `ClientConnection.OnReceived` decompresses GZip payloads without a decompressed-size limit: `src/Perpetuum/Network/ClientConnection.cs:28`
+
+**Impact:** Brute-force login attacks, connection flooding, and potential decompression-bomb DoS.
+
+**Fix approach:** Add per-IP connection rate limiting in `SessionManager`; add decompressed-size cap before `GZip.Decompress`.
+
+---
+
+### ProcessManager Uses a Custom Game Loop Thread (Not .NET Task Scheduler)
+
+`ProcessManager` runs all zone and service updates in a single `Thread("MainLoop")` with manual `Thread.Sleep`:
+- `src/Perpetuum/Threading/Process/ProcessManager.cs`
+
+`ThreadAbortException` is still explicitly caught (`ProcessManager.cs:99`) — a pattern deprecated since .NET Core.
+
+**Impact:** Thread.Abort is no longer thrown in .NET 5+; that catch block is dead code. Any exception in a process `Update()` that is not caught internally will silently swallow the error and advance to the next tick.
+
+**Fix approach:** Remove the `ThreadAbortException` catch. Add per-process exception isolation so one failing process does not corrupt the loop timing.
+
+---
+
+### Fire-and-Forget `Task.Delay().ContinueWith()` Without Cancellation
+
+22 occurrences of fire-and-forget deferred actions (weapon flight time, NPC spawns, beam expiry, SAP enter delays) use `Task.Delay(...).ContinueWith(t => ...)` with no cancellation token:
+
+- `src/Perpetuum/Modules/Weapons/WeaponModule.cs:171,218`
+- `src/Perpetuum/Zones/Eggs/AreaBomb.cs:41`
+- `src/Perpetuum/Zones/Intrusion/Outpost.cs:233,392,590`
+- `src/Perpetuum/Zones/NpcSystem/NPCBossInfo.cs:220`
+
+**Impact:** If a zone shuts down or a unit is destroyed mid-flight, the callback executes against a no-longer-valid zone state. This can cause null reference exceptions or phantom damage.
+
+**Fix approach:** Pass `CancellationToken` from the zone or unit's lifecycle into `Task.Delay`.
+
+---
+
+### Widespread `#if DEBUG` Behavioral Divergence
+
+The `MissionHandler`, `MissionInProgress`, `MissionProcessorDeliverMission`, `ProductionProcessor`, and many other core systems have extensive `#if DEBUG` / `#if !DEBUG` blocks (50+ occurrences) that alter logic, not just logging. The most concerning is `MissionHandler.cs:554`: `#if !DEBUG` wraps a code path that only runs in production.
+
+**Impact:** Release behavior diverges from what developers test locally. Bugs that only manifest in Release builds are harder to reproduce and diagnose.
+
+**Fix approach:** Replace behavioral `#if DEBUG` blocks with configuration flags (`GlobalConfiguration`) or feature flags so they are testable in any build.
+
+---
+
+## Missing Infrastructure
+
+### No Automated Tests
+
+The repository has zero automated tests (unit, integration, or functional). CLAUDE.md states this explicitly.
+
+**Impact:** Every change to core systems — combat calculations, mission rewards, market transactions, season point accrual — must be manually validated in a running server with a connected database. Regressions are invisible until they reach production or are found by players.
+
+**Fix approach:** Start with pure-logic unit tests for `FastRandom`, economy formulas, mission reward calculations, and season point math. These have no external dependencies and provide immediate value.
+
+---
+
+### No Structured Logging or Observability
+
+The server uses a custom `Logger` implementation backed by flat file logs and a custom `ILogger` interface. There is no:
+- Structured log format (JSON, etc.)
+- Correlation IDs across requests
+- Distributed tracing
+- Metrics (request count, zone tick latency, DB query duration)
+- Error aggregation service (Sentry, Seq, etc.)
+
+**Impact:** Diagnosing production issues requires manually tailing flat log files. Latency spikes and error rate trends are invisible.
+
+**Fix approach:** Integrate Serilog with a file sink (structured JSON) and optionally a Seq or Elastic sink. Add zone tick duration metrics.
+
+---
+
+### CI Pipeline Does Not Test
+
+`.github/workflows/dotnet.yml` only builds `Perpetuum.ServerService2`; it does not run tests, static analysis, or linting. The workflow only triggers on the `develop` branch, not `main`.
+
+**Impact:** Pull requests to branches other than `develop` are not validated. No automated quality gate exists.
+
+**Fix approach:** Extend CI to run `dotnet build` on all PRs regardless of target branch. Add `dotnet test` once tests exist. Consider adding Roslyn analyzers.
+
+---
+
+### `Nullable` Set to `annotations` (Not `enable`) Across All Projects
+
+All `.csproj` files use `annotations ` rather than `enable`. This means nullable annotations are present but the compiler does not enforce them — `[CanBeNull]` / `[NotNull]` are decorative only.
+
+**Impact:** Null safety is not enforced at compile time. Null dereferences can still occur even where annotations are correct.
+
+**Fix approach:** Migrate to `enable ` project by project, fixing warnings incrementally.
+
+---
+
+### Chat Logs Committed to Source Repository
+
+The `src/Perpetuum.ServerService2/data/chatlogs/` directory contains production chat log files from 2024–2025. Git history includes player chat from the `Syndicate Radio` channel.
+
+**Impact:** Player communication data is exposed in the public repository history. Depending on jurisdiction, this may conflict with data privacy obligations.
+
+**Fix approach:** Add `src/Perpetuum.ServerService2/data/` to `.gitignore` and purge historical entries from git history with `git-filter-repo`.
+
+---
+
+## Security Considerations
+
+### Plaintext Passwords in Database
+- Risk: Full credential exposure on DB breach.
+- Files: `src/Perpetuum/Accounting/AccountRepository.cs`
+- Current mitigation: None.
+- Recommendation: Implement bcrypt hashing immediately.
+
+### Weak Network Cipher
+- Risk: Game traffic decryptable by a passive observer on the network.
+- Files: `src/Perpetuum/Network/EncryptedTcpConnection.cs`
+- Current mitigation: RC4 layer in `ClientConnection.cs` for admin/test client only.
+- Recommendation: TLS at the TCP listener level or per-session AES key agreement.
+
+### No Login Brute-Force Protection
+- Risk: Password enumeration via repeated login attempts.
+- Files: `src/Perpetuum/Services/Sessions/SessionManager.cs`
+- Current mitigation: None (MaxSessions is a total cap, not a per-IP rate limit).
+- Recommendation: Track failed logins per IP/account; lock after threshold.
+
+### GZip Decompression Without Size Limit
+- Risk: A client can send a small compressed packet that expands to gigabytes (decompression bomb).
+- Files: `src/Perpetuum/Network/ClientConnection.cs:28`
+- Current mitigation: None.
+- Recommendation: Cap decompressed output at a sane maximum (e.g., 4 MB).
+
+### Discord Bot Token in `perpetuum.ini`
+- Risk: The `DiscordBotToken` field in `GlobalConfiguration` is stored in plaintext in the `perpetuum.ini` config file inside `GameRoot`. If that directory is accidentally committed or exposed, the bot token leaks.
+- Files: `src/Perpetuum/GlobalConfiguration.cs:50`, `src/Perpetuum/Services/EventServices/EventListenerService.cs:123`
+- Current mitigation: Config is gitignored at `GameRoot` level.
+- Recommendation: Support environment variable override for secrets; document this requirement.
+
+---
+
+## Performance Hotspots
+
+### `SeasonService.RecordActivity` Called on Every Kill / Mine / Mission
+
+`RecordActivity` runs on the hot path for NPC kills, PvP kills, mining, and mission completions. Each call:
+1. Iterates `_activeRates` (LINQ `.Where` + `.ToList`)
+2. Iterates `_activeObjectives` (LINQ `.Where`)
+3. Issues 2–4 DB writes per matched objective (`AddPoints`, `IncrementObjectiveProgress`, `MarkObjectiveBonusAwarded`)
+4. Queries `GetClaimedTierIds` per call (full DB round-trip)
+
+- Files: `src/Perpetuum/Services/Seasons/SeasonService.cs:118-165`
+
+**Impact:** Under load with many concurrent kills, this is a significant per-kill DB write amplification.
+
+**Fix approach:** Cache `GetClaimedTierIds` per character in memory; batch `AddPoints` writes; avoid `ToList()` in hot path.
+
+---
+
+### `FastRandom` Uses a Global SpinLock
+
+`FastRandom` is a global singleton with a `SpinLock` protecting its state:
+- `src/Perpetuum/FastRandom.cs:18`
+
+It is called 151+ times across zones, combat, and NPC AI — often from concurrent zone threads.
+
+**Impact:** SpinLock contention under multi-zone load. SpinLocks are appropriate only for very short critical sections; if the calling thread is preempted while spinning, CPU is wasted.
+
+**Fix approach:** Use `[ThreadStatic]` per-thread random instances, or switch to `System.Random.Shared` (.NET 6+).
+
+---
+
+### NPC AI Uses `Task.Result` to Block on Pathfinding
+
+Multiple NPC AI classes synchronously block on pathfinding futures using `.Result`:
+- `src/Perpetuum/Zones/NpcSystem/AI/CombatAI.cs:297`
+- `src/Perpetuum/Zones/NpcSystem/AI/CombatDrones/CombatDroneAI.cs:195`
+- `src/Perpetuum/Zones/NpcSystem/AI/FleeAI.cs:107`
+- 8 more files
+
+This blocks the zone's `ProcessManager` thread while waiting for pathfinding to complete.
+
+**Impact:** If pathfinding is slow (large zones, complex terrain), the entire zone update loop stalls.
+
+**Fix approach:** Return from AI `Update()` without a path and pick up the result next tick using a stored `Task>`.
+
+---
+
+### 104 `SELECT *` Queries Including Full Table Scans via `Database.CreateCache`
+
+`Database.CreateCache` and `Database.CreateLookupCache` always issue `select * from {table}`:
+- `src/Perpetuum/Data/Database.cs:33,40`
+
+These caches are populated at startup and are lazy-loaded, but individual call sites pass large tables (entity definitions, NPC data).
+
+**Impact:** Over-fetching on startup; risk of performance regression if new columns with large data are added to cached tables.
+
+---
+
+## Operational Concerns
+
+### No Graceful Shutdown Procedure
+
+The `ProcessManager.Stop()` forces the game loop thread to terminate with a 5-second join timeout. There is no protocol to:
+- Notify connected players of impending shutdown
+- Flush in-flight transactions
+- Drain the `EventListenerService` queue
+- Wait for active Discord async operations to complete
+
+**Impact:** Players can be disconnected mid-transaction on a server restart; item or mission state may be inconsistent.
+
+**Fix approach:** Add a shutdown countdown broadcast (command already exists: `HostShutDownManager`), and wait for transaction draining before stopping the loop.
+
+---
+
+### Configuration Has No Schema Validation
+
+`perpetuum.ini` is deserialized via `JsonConvert.DeserializeObject` with no validation. Missing or malformed fields produce null/default values silently:
+- `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs:339-340`
+
+**Impact:** Misconfiguration causes cryptic runtime failures far from the config load point.
+
+**Fix approach:** Add required-field validation on `GlobalConfiguration` after deserialization; fail fast at startup with descriptive messages.
+
+---
+
+### Production Chat Logs and Runtime Data in Source Tree
+
+`src/Perpetuum.ServerService2/data/` contains runtime artifacts:
+- `chatlogs/` — actual game chat (2024–2025)
+- `logs/` — server log files
+- `layers/` — zone terrain bitmaps
+- `database/` — database-related data
+
+These are checked into git history.
+
+**Impact:** Repository bloat; potential player privacy exposure; difficult to clone for new contributors.
+
+---
+
+### Single-Machine, Windows-Only Deployment
+
+The server explicitly targets `x64` and Windows only. There is no containerization, no horizontal scaling path, and zone processes share the same host. The `Open.Nat` library in the solution suggests NAT traversal is attempted automatically.
+
+**Impact:** No path to cloud deployment or Linux hosting. A single hardware failure takes down all zones.
+
+---
+
+## Recommendations (Prioritized)
+
+1. **[Critical] Hash passwords** — implement bcrypt in `AccountRepository` before any public exposure of the server. This is a blocking security requirement.
+
+2. **[High] Add decompression bomb guard** — cap decompressed packet size in `ClientConnection.OnReceived` to prevent DoS.
+
+3. **[High] Add login rate limiting** — track failed auth attempts per IP/account in `SessionManager`; block after a configurable threshold.
+
+4. **[High] Remove production data from git** — purge `src/Perpetuum.ServerService2/data/chatlogs/` and `data/logs/` from git history; add to `.gitignore`.
+
+5. **[High] Provide cancellation tokens to `Task.Delay` fire-and-forget calls** — prevents phantom callbacks after zone teardown.
+
+6. **[Medium] Replace `ThreadAbortException` catch** in `ProcessManager` — it is dead code on .NET 8 and masks real issues.
+
+7. **[Medium] Cache `GetClaimedTierIds` per character** in `SeasonService.RecordActivity` — reduces per-kill DB write amplification.
+
+8. **[Medium] Replace `FastRandom` global SpinLock** with `[ThreadStatic]` instances or `System.Random.Shared`.
+
+9. **[Medium] Migrate nullable to `enable`** project by project — enforces null safety at compile time.
+
+10. **[Low] Fix NPC AI pathfinding blocking** — store `Task>` across ticks instead of blocking on `.Result`.
+
+11. **[Low] Move hardcoded economy constants to DB** — alpha/beta multipliers, mission reward parameters flagged with TODO.
+
+12. **[Low] Add startup config validation** for `GlobalConfiguration` — fail fast on missing required fields.
+
+13. **[Low] Introduce structured logging** (Serilog) — prerequisite for meaningful observability.
+
+14. **[Low] Add unit tests for pure-logic modules** — season point math, mission reward formulas, economy calculations — as a foundation for future confidence.
+
+---
+
+*Concerns audit: 2026-05-11*
diff --git a/docs/codebase/CONVENTIONS.md b/docs/codebase/CONVENTIONS.md
new file mode 100644
index 0000000..b80749d
--- /dev/null
+++ b/docs/codebase/CONVENTIONS.md
@@ -0,0 +1,178 @@
+# Coding Conventions
+
+**Analysis Date:** 2026-05-11
+
+## Naming Conventions
+
+**Classes:**
+- PascalCase throughout: `SeasonService`, `MarketHandler`, `ZoneSession`, `RequestHandlerProfiler`
+- Interfaces prefixed with `I`: `IRequestHandler`, `IZone`, `ISeasonService`, `IProcess`, `IReadOnlyRepository`
+- Abstract base classes without prefix: `Process`, `Entity`, `Module`
+- Handler classes named `` or ``: `MarketHandler`, `ChangeAmmo`, `SignIn`, `AccountList`
+- Repository classes named `Repository`: `SeasonRepository`, `MarketOrderRepository`, `CharacterProfileRepository`
+- Service classes named `Service`: `SeasonService`, `MarketCleanUpService`, `EPBonusEventService`
+- Exception classes suffixed `Exception`: `PerpetuumException`
+- Enum types in PascalCase: `ErrorCodes`, `ModuleStateType`, `SeasonActivityType`
+
+**Methods:**
+- PascalCase public methods: `HandleRequest`, `RefreshCache`, `AddPoints`, `GetActiveSeason`
+- PascalCase private methods: `LoadTerminalPositions`, `FlushLogInfos`, `OnSessionAdded`
+- Event handler methods prefixed `On`: `OnStateChanged`, `OnSessionAdded`, `OnCharacterLogin`, `OnDynamicPropertiesUpdated`
+- Factory methods named `Create` or `Get`: `PerpetuumException.Create(error)`, `Db.Query()`
+- Boolean-returning methods use `Is`, `Has`, `Can`, `Try` prefixes: `IsSellable`, `IsAmmoable`, `TryGet`, `TryMarkIntroMailSent`
+
+**Fields:**
+- Private fields prefixed with underscore, camelCase: `_repository`, `_sessionManager`, `_activeSeason`
+- Public static readonly fields (constants/singletons) use PascalCase: `EntityDefault.None`, `ZoneSession.None`
+- Static fields for factories/services use PascalCase: `Entity.Services`, `EntityDefault.Reader`
+- Enum-like static fields on classes use PascalCase: `ResolveTestTaskCreationOptions`
+
+**Properties:**
+- PascalCase public auto-properties: `Id`, `Name`, `IsActive`, `Configuration`
+- Backing fields use underscore prefix: `_owner` backing `Owner`, `_health` backing `Health`
+
+**Parameters:**
+- camelCase: `characterId`, `seasonId`, `objectiveId`, `commandText`
+
+**Type Parameters:**
+- Single-letter `T`, or descriptive `TId`, `TKey`, `TValue`
+
+**Keys / Constants:**
+- String keys for request data use the `k.*` constant pattern (e.g. `k.characterID`, `k.robotEID`, `k.zone`). Defined in a static class `k` in `src/Perpetuum/`
+
+## Code Organization
+
+**File naming:**
+- One public type per file, file name matches type name
+- Partial classes split across multiple files with suffix after dot: `Robot.cs`, `Robot.Helpers.cs`, `Robot.Locking.cs`, `Robot.Properties.cs`; also `ActiveModule.cs`, `ActiveModule.States.cs`, `ActiveModule.Ammo.cs`
+- Interface files named `I.cs`: `IZone.cs`, `IProcess.cs`, `ISeasonService.cs`
+
+**Namespace structure:**
+- Root namespace: `Perpetuum`
+- Subsystem namespaces: `Perpetuum.Services.Seasons`, `Perpetuum.Zones.NpcSystem`, `Perpetuum.RequestHandlers.Markets`
+- Mirrors directory structure under `src/Perpetuum/`
+
+**Class organization within files:**
+- Section dividers use `// ── Section Name ──` comment style (seen in `SeasonRepository.cs`, `SeasonService.cs`)
+- Static singletons/factories defined at top of class
+- Constructor follows fields
+- Public API before private helpers
+
+**Static service locators:**
+- `Entity.Services` (static injection point), `EntityDefault.Reader`, `Db.DbQueryFactory`, `Logger.Current` — these are static properties set during bootstrapping rather than constructor injection in some core classes
+
+## Patterns in Use
+
+**Command/Handler pattern:**
+- Each client command maps 1:1 to a handler class implementing `IRequestHandler` (or `IRequestHandler` for zone-scoped commands)
+- Handlers registered via Autofac in `src/Perpetuum.Bootstrapper/Modules/RequestHandlersModule.cs` and `ZoneRequestHandlersModule.cs`
+- Handler `HandleRequest(IRequest request)` is the single entry point; data extracted via `request.Data.GetOrDefault(k.key)`
+
+**Repository pattern:**
+- `IRepository` / `IReadOnlyRepository` interfaces
+- Concrete implementations query via `Db.Query()` fluent API
+- `CachedReadOnlyRepository` wraps any repository with `ObjectCache`-backed caching (`src/Perpetuum/CachedReadOnlyRepository.cs`)
+
+**Process/game-loop pattern:**
+- Long-running services extend `Process` (`src/Perpetuum/Threading/Process/Process.cs`) and override `Update(TimeSpan time)`
+- `ProcessManager` owns a dedicated `Thread` ("MainLoop") that calls `Update` on all registered processes on a fixed interval
+- Zone processes and services register with `IProcessManager` via Autofac
+
+**State machine pattern:**
+- `StackFSM` (stack-based FSM) used for module states in `src/Perpetuum/StateMachines/`
+- States implement `IState`; `IModuleState` wraps FSM states for module behavior
+- Module state types enumerated in `ModuleStateType` enum
+
+**Builder pattern:**
+- `IBuilder` with `Build()` method in `src/Perpetuum/Builders/IBuilder.cs`
+- `Message.Builder.FromRequest(request).WithData(d).WrapToResult().Send()` — fluent builder for wire responses
+
+**Guard / fluent-throw pattern:**
+- `src/Perpetuum/Guard.cs` provides extension methods for inline validation:
+ - `value.ThrowIfNull(ErrorCodes.X)`
+ - `value.ThrowIfFalse(ErrorCodes.X)`
+ - `value.ThrowIfTrue(ErrorCodes.X)`
+ - `value.ThrowIfEqual(comparand, ErrorCodes.X)`
+ - `value.ThrowIfNotEqual(comparand, ErrorCodes.X)`
+ - `value.ThrowIfLess(comparand, ErrorCodes.X)` / `ThrowIfGreater`
+ - `value.ThrowIfNotType(ErrorCodes.X)`
+ - `errorCode.ThrowIfError()`
+- These are the **standard** way to validate preconditions in handlers and services
+
+**Observer / event pattern:**
+- `IObservable` / `IObserver` used for event messages (`IEventProcessor : IObserver`)
+- C# events used for session lifecycle: `SessionAdded`, `CharacterSelected`
+
+**Dependency injection:**
+- Autofac; all services injected via constructor
+- Autofac modules in `src/Perpetuum.Bootstrapper/Modules/` — one module per subsystem
+- Some core singletons use static service locator during startup (`Entity.Services`, `Logger.Current`) for legacy reasons
+
+**Immutable state for thread safety:**
+- `ImmutableList` with `ImmutableInterlocked.Update` for `ProcessManager._processes`
+- `volatile` fields for frequently-read single values: `_activeSeason`, `_lastNotifiedSeasonId`
+- `ImmutableHashSet` for entity children collection
+
+## Error Handling
+
+**Domain errors:**
+- `PerpetuumException(ErrorCodes error)` is the standard domain exception
+- `ErrorCodes` enum in `src/Perpetuum/ErrorCodes.cs` enumerates all possible game-logic error conditions
+- Throw via `PerpetuumException.Create(error)` or `throw new PerpetuumException(ErrorCodes.X)`
+- Contextual data attached via `.SetData(key, value)` chaining on `PerpetuumException`
+- Common pattern: `someValue.ThrowIfNull(ErrorCodes.ItemNotFound)` — see Guard pattern above
+
+**System/infrastructure errors:**
+- `Logger.Exception(ex)` logs exceptions without rethrowing
+- `Task.LogExceptions()` extension on `TaskExtensions` (`src/Perpetuum/TaskExtensions.cs`) attaches a fault-continuation that logs `AggregateException` inner exceptions
+- Fire-and-forget tasks use `.LogExceptions()`: `Task.Run(OnDisconnected).ContinueWith(t => Dispose()).LogExceptions()`
+- `try/catch (PerpetuumException gex)` at handler call sites to send error responses to client
+
+**Transaction handling:**
+- Database mutations wrapped in `using (var scope = Db.CreateTransaction()) { ... scope.Complete(); }`
+- `Transaction.Current.OnCompleted(completed => { ... })` used to send client responses only after DB commit succeeds
+
+## Async Patterns
+
+**Primary threading model:**
+- Not async/await-based. The codebase uses `Task.Run`, `Task.Factory.StartNew`, `Thread`, and `ThreadPool` directly
+- `async`/`await` appears only in `EventListenerService.cs` for the Discord.NET client (external SDK requirement)
+- No `ConfigureAwait` usage — async is not the dominant pattern
+
+**Fire-and-forget:**
+- `Task.Run(() => ...).LogExceptions()` — standard pattern for background work that should not block the caller
+- `Task.Run(...).ContinueWith(t => ...)` — continuation chaining for post-async actions
+
+**Parallel work:**
+- `Task.Factory.StartNew(..., ResolveTestTaskCreationOptions, TaskScheduler.Default)` for CPU-bound parallel batch work (mission resolution testing)
+- `Task.WaitAll(tasks.ToArray())` for joining parallel batches
+
+**ThreadPool direct:**
+- `ThreadPool.UnsafeQueueUserWorkItem(_ => ..., null)` used in `MessageSender` and `TcpConnection` for low-latency network dispatch
+
+**Game loop:**
+- `ProcessManager` runs a named background thread ("MainLoop") that tick-calls all `IProcess.Update(TimeSpan)` — this is the primary game simulation clock
+
+## Comments & Documentation
+
+**XML doc comments:**
+- Used selectively on public APIs, especially utilities and domain types
+- ``, ` `, `` tags present on ~250 files
+- Not exhaustive — many handler classes have no XML docs
+
+**Inline comments:**
+- Section dividers in long files: `// ── Section Name ────────────────────────────────────────────────────`
+- Clarifying comments on non-obvious logic: `//target module is empty`, `//clean pbshighway bit`
+- `//... other conditions` placeholder-style comments exist (low-quality legacy areas)
+
+**JetBrains annotation attributes:**
+- `[NotNull]`, `[CanBeNull]` from a local copy of JetBrains annotations in `src/Perpetuum/Annotations.cs`
+- `[UsedImplicitly]`, `[Pure]`, `[InstantHandle]` used throughout
+- These are compile-time documentation and static analysis hints only — no runtime enforcement
+
+**`DEPRECATED_` prefix convention:**
+- Enum members in `ErrorCodes` that are obsolete are prefixed `DEPRECATED_` rather than removed, to preserve numeric values
+
+---
+
+*Convention analysis: 2026-05-11*
diff --git a/docs/codebase/INTEGRATIONS.md b/docs/codebase/INTEGRATIONS.md
new file mode 100644
index 0000000..00179c8
--- /dev/null
+++ b/docs/codebase/INTEGRATIONS.md
@@ -0,0 +1,88 @@
+# External Integrations
+
+**Analysis Date:** 2026-05-11
+
+## Databases
+
+**Microsoft SQL Server:**
+- The sole data store for all game state, accounts, entities, missions, market, production, standings, characters, corporations, seasons, and more
+- Connection string stored in `perpetuum.ini` (GameRoot directory), surfaced via `GlobalConfiguration.ConnectionString`
+- Client: `Microsoft.Data.SqlClient` 6.0.1
+- Access layer: `src/Perpetuum/Data/DbQuery.cs` (fluent query builder), `src/Perpetuum/Data/Db.cs` (static factory), `src/Perpetuum/Data/Database.cs` (table cache utilities)
+- Auto-detects inline SQL vs stored procedures by presence of spaces in command text
+- Transactions: `System.Transactions.TransactionScope` with `ReadCommitted` isolation; distributed transactions enabled at startup in `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs`
+- Schema documentation: `docs/db_structure/` — authoritative source; includes tables, stored procedures, views, functions, data types
+- Admin tool (`src/Perpetuum.AdminTool/`) connects directly using its own `Microsoft.Data.SqlClient` reference
+
+## Network Protocols
+
+**Custom game protocol (GenXY / GenxyString):**
+- Binary wire format defined in `src/Perpetuum/GenXY/`
+- `GenxyReader` / `GenxyWriter` for serialization; `GenxyConverter` for registering custom type converters
+- All client ↔ server game traffic uses this protocol
+- Type converters registered at startup (e.g., `Character` → `int` id)
+
+**Encrypted TCP:**
+- `src/Perpetuum/Network/EncryptedTcpConnection.cs` — XOR stream cipher with rolling key (fixed seed bytes `0xCA`/`0xAC`)
+- `src/Perpetuum/Network/TcpConnection.cs` — base TCP framing
+- `src/Perpetuum/Network/ClientConnection.cs` — client session wrapper
+- Game listens on port defined by `GlobalConfiguration.ListenerPort`; each zone has its own additional listener port (configured in `perpetuum.ini`)
+
+**HTTP (outbound):**
+- `src/Perpetuum/Network/Http.cs` — simple `WebClient.UploadValues` POST helper (`User-Agent: PerpetuumServer/1.0`)
+- Used for webhook and external notification calls (deprecated `WebClient` API; suppresses `SYSLIB0014` warning)
+- Discord webhook integration: `GlobalConfiguration.WebHookId` and `GlobalConfiguration.WebHookOAuth`
+
+**Cryptography:**
+- RSA: custom implementation at `src/Perpetuum/Rsa.cs` — hardcoded modulus key bytes; used for client authentication handshake
+- RC4: custom stream cipher at `src/Perpetuum/Rc4.cs` — used for session key exchange
+
+## External Services
+
+**Steam (Valve):**
+- Native DLL interop: `sdkencryptedappticket64.dll` (Steamworks SDK, x64)
+- Used to authenticate players via encrypted app tickets
+- Implementation: `src/Perpetuum/Services/Steam/SteamHelper.cs`
+- Config: `GlobalConfiguration.SteamAppID` (int) and `GlobalConfiguration.SteamKey` (byte[])
+- The DLL must be present in the working directory; P/Invoke calls via `[DllImport]`
+
+**Discord:**
+- Library: `Discord.Net` 3.17.4 (`DiscordSocketClient`)
+- Gateway intents: `AllUnprivileged | MessageContent`
+- Bidirectional bridge between in-game chat channels and Discord channels
+- Perpetuum → Discord: messages forwarded in `src/Perpetuum/Services/EventServices/EventListenerService.cs`
+- Discord → Perpetuum: handled by `src/Perpetuum/Services/EventServices/EventProcessors/DiscordIntegrationHandler.cs`
+- Channel mapping stored in DB via `IChannelManager.GetChannelNameByDiscordId()`
+- Config: `GlobalConfiguration.DiscordBotToken`, `GlobalConfiguration.OpHelpChannelId`, `GlobalConfiguration.WebHookId`, `GlobalConfiguration.WebHookOAuth`
+- Integration is optional; bot token left empty disables it
+
+**UPnP / NAT Traversal:**
+- Library: `SharpOpenNat` 4.0.17
+- Attempts to map game and zone TCP ports on router via UPnP protocol
+- Controlled by `GlobalConfiguration.EnableUpnp` (opt-in)
+- Wired in `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs` (`TryInitUpnp`)
+
+## File System
+
+**GameRoot directory** (path from `appsettings.json` → `GlobalConfiguration.GameRoot`):
+- `perpetuum.ini` — main server config: SQL connection string, ports, zone config, feature flags (stored as `GlobalConfiguration` JSON)
+- Zone terrain data and map files — loaded by `src/Perpetuum/Zones/Terrains/`
+- Robot templates and definition data — referenced at startup via `EntityDefault` / `IEntityDefaultReader`
+- `src/Perpetuum/IO/IFileSystem.cs` — abstraction over file I/O; implementations at `src/Perpetuum/IO/FileSystem.cs`
+
+**Log files:**
+- Custom file logger: `src/Perpetuum/Log/Loggers/FileLogger.cs`
+- Composite logger: `src/Perpetuum/Log/Loggers/CompositeLogger.cs` — fan-out to multiple targets (file, console, buffered)
+- Channel chat logs: `src/Perpetuum/Services/Channels/ChannelLogger.cs`
+- Corporation action logs: `src/Perpetuum/Common/Loggers/` (transaction logger)
+
+**Admin Tool data files** (`src/Perpetuum.ServerService2/`):
+- `admincreds.json` — admin credentials for the WPF Admin Tool (local only, not committed to production)
+- `localserverinfo.json` — local server metadata
+
+**Build output:**
+- `bin/x64/Release/net8.0/` — all compiled assemblies and native DLLs (including `sdkencryptedappticket64.dll`)
+
+---
+
+*Integration audit: 2026-05-11*
diff --git a/docs/codebase/STACK.md b/docs/codebase/STACK.md
new file mode 100644
index 0000000..40d2f2a
--- /dev/null
+++ b/docs/codebase/STACK.md
@@ -0,0 +1,99 @@
+# Technology Stack
+
+**Analysis Date:** 2026-05-11
+
+## Languages & Runtimes
+
+**Primary:**
+- C# 12 / .NET 8 — all server and tool projects
+- SQL (T-SQL) — stored procedures, views, functions in `docs/db_structure/`
+
+**Secondary:**
+- XAML — WPF Admin Tool UI (`src/Perpetuum.AdminTool/`)
+
+**Runtime:**
+- .NET 8 (`net8.0`), except Admin Tool which targets `net8.0-windows`
+- Platform: x64 only (all `.csproj` files set `x64 `)
+- OS: Windows only (bootstrapper annotated `[SupportedOSPlatform("windows")]`)
+- GC: Server GC enabled in `Perpetuum.ServerService2` (`true `)
+
+## Frameworks & Libraries
+
+**Dependency Injection:**
+- `Autofac` 8.2.0 — entire DI container; registered in 19 `Modules/*.cs` under `src/Perpetuum.Bootstrapper/Modules/`
+
+**Windows Service Host:**
+- `Microsoft.Extensions.Hosting` 9.0.1 — generic host abstraction
+- `Microsoft.Extensions.Hosting.WindowsServices` 9.0.1 — Windows service integration in `Perpetuum.ServerService2`
+
+**Database Access:**
+- `Microsoft.Data.SqlClient` 6.0.1 — raw ADO.NET; no ORM. Used in `src/Perpetuum/Data/DbQuery.cs` and `src/Perpetuum.AdminTool/`
+
+**Serialization:**
+- `Newtonsoft.Json` 13.0.3 — JSON config parsing (`GlobalConfiguration`, `perpetuum.ini`)
+- Custom binary protocol: GenXY (`src/Perpetuum/GenXY/`) — `GenxyReader`, `GenxyWriter`, `GenxyConverter`, `GenxyToken`
+
+**Discord Integration:**
+- `Discord.Net` 3.17.4 — bot client (`DiscordSocketClient`) for bidirectional chat bridge; see `src/Perpetuum/Services/EventServices/EventListenerService.cs`
+
+**WPF / Admin Tool:**
+- `CommunityToolkit.Mvvm` 8.3.2 — MVVM helpers for `src/Perpetuum.AdminTool/`
+
+**Networking / NAT:**
+- `SharpOpenNat` 4.0.17 — UPnP port mapping; optional, controlled by `GlobalConfiguration.EnableUpnp`
+
+**Utilities:**
+- `DeepCloner` 0.10.4 — deep object cloning in core game logic
+- `System.Drawing.Common` 9.0.1 — terrain bitmap processing in `src/Perpetuum/Zones/`
+- `System.Runtime.Caching` 9.0.1 — `ObjectCache` for character and entity caching
+- `System.Net.Http` 4.3.4 — HTTP post utility in `src/Perpetuum/Network/Http.cs`
+- `System.Text.RegularExpressions` 4.3.1 — regex utilities (referenced in all projects)
+
+**CLI:**
+- `McMaster.Extensions.CommandLineUtils` 4.1.1 — `--GameRoot` argument parsing in `src/Perpetuum.Server/Program.cs`
+
+**Crypto:**
+- RC4 stream cipher: custom implementation at `src/Perpetuum/Rc4.cs`
+- RSA: custom implementation at `src/Perpetuum/Rsa.cs`
+- XOR stream cipher: custom; see `src/Perpetuum/Network/EncryptedTcpConnection.cs`
+
+## Build & Tooling
+
+**Build System:**
+- `dotnet build` with MSBuild
+- Solution file: `PerpetuumServer2.sln`
+- Release command: `dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64`
+- Output directory: `bin/x64/Release/net8.0` (set via `..\..\bin ` in `Perpetuum.ServerService2`)
+
+**CI/CD:**
+- GitHub Actions: `.github/workflows/dotnet.yml`
+- Runner: `windows-latest`
+- Trigger: push/PR to `develop` branch
+- Publishes build artifact `Perpetuum-Server-v2-{sha}` on push
+- Only `Perpetuum.ServerService2` project is built in CI
+
+**No automated tests** — no test project, no test framework configured.
+
+**Unsafe code:**
+- `true ` in `src/Perpetuum/Perpetuum.csproj`
+
+## Package Management
+
+**Package manager:** NuGet (standard `` in `.csproj` files)
+**Lockfile:** Not detected (no `packages.lock.json` committed)
+**Legacy packages folder:** `packages/` directory exists at repo root (may be legacy)
+
+## Database
+
+**Engine:** Microsoft SQL Server
+**Access pattern:** Raw ADO.NET via custom `DbQuery` fluent builder — no ORM
+- `src/Perpetuum/Data/DbQuery.cs` — query builder with `CommandText`, `SetParameter`, `Execute`
+- `src/Perpetuum/Data/Db.cs` — static factory facade (`Db.Query()`, `Db.CreateTransaction()`)
+- `src/Perpetuum/Data/Database.cs` — `LazyDictionary` / `LazyLookup` table-cache utilities
+- Auto-detects stored procedures vs inline SQL: command text without spaces → `CommandType.StoredProcedure`
+- Transaction isolation: `ReadCommitted` via `TransactionScope`; distributed transactions enabled (`TransactionManager.ImplicitDistributedTransactions = true`)
+- Schema docs: `docs/db_structure/` (authoritative; see CLAUDE.md)
+
+---
+
+*Stack analysis: 2026-05-11*
diff --git a/docs/codebase/STRUCTURE.md b/docs/codebase/STRUCTURE.md
new file mode 100644
index 0000000..8938874
--- /dev/null
+++ b/docs/codebase/STRUCTURE.md
@@ -0,0 +1,316 @@
+# Project Structure
+
+**Analysis Date:** 2026-05-11
+
+## Solution Layout
+
+```
+PerpetuumServer2.sln
+├── src/Perpetuum/ # Core library — all game logic
+├── src/Perpetuum.Bootstrapper/ # Autofac DI wiring
+├── src/Perpetuum.RequestHandlers/ # 150+ command handler classes
+├── src/Perpetuum.ExportedTypes/ # Shared enum/type definitions
+├── src/Perpetuum.Server/ # Console entry point
+├── src/Perpetuum.ServerService2/ # Windows service wrapper
+├── src/Perpetuum.ServerService2Installer/ # Windows service installer
+├── src/Perpetuum.AdminTool/ # WPF admin/tooling application
+├── src/Open.Nat/ # UPnP library (vendored)
+├── docs/ # DB schema docs (authoritative)
+│ └── db_structure/
+│ ├── database_schema_documentation.md
+│ ├── stored_procedures/
+│ ├── functions/
+│ ├── views/
+│ └── data_types/
+└── packages/ # NuGet package cache
+```
+
+## Project Roles
+
+| Project | Role |
+|---------|------|
+| `Perpetuum` | All game logic: entities, zones, modules, robots, players, services |
+| `Perpetuum.Bootstrapper` | Autofac container assembly; one `Module` per subsystem |
+| `Perpetuum.RequestHandlers` | Handler class per command; no game logic, thin orchestration |
+| `Perpetuum.ExportedTypes` | Enums/flags shared across projects (`AggregateField`, `CategoryFlags`, `EffectType`, etc.) |
+| `Perpetuum.Server` | `Program.cs` — parses CLI args, calls `PerpetuumBootstrapper.Init/Start` |
+| `Perpetuum.ServerService2` | Windows service host wrapping bootstrapper |
+| `Perpetuum.AdminTool` | Standalone WPF tool for admin operations (NPC editing, seasons, loot, etc.) |
+
+## Source Organisation — `src/Perpetuum/`
+
+```
+src/Perpetuum/
+├── EntityFramework/ # Entity, EntityDefault, DynamicProperty, EntityFactory, EntityRepository
+├── Units/ # Unit base class, UnitOptionalProperty, DockingBases, FieldTerminals
+├── Robots/ # Robot, RobotComponent, RobotHead/Leg/Chassis, RobotSetup, Fitting
+├── Players/ # Player class
+├── Modules/ # Module, ActiveModule (+States), 40+ concrete module types
+│ ├── Weapons/
+│ ├── EffectModules/
+│ ├── Terraforming/
+│ ├── RemoteControl/
+│ ├── AdaptiveAlloy/
+│ └── ModuleProperties/
+├── Zones/ # Zone, ZoneManager, ZoneSession, IZone, ZoneConfiguration
+│ ├── NpcSystem/ # Npc, SmartCreature, AI, CrystalAI, Flocks, Presences, …
+│ ├── PBS/ # Player-built structures (docking bases, turrets, towers, …)
+│ ├── Terrains/ # ITerrain, layers (altitude/control/blocking), materials, plants
+│ ├── Intrusion/ # SAP mechanics, outpost stability, intrusion events
+│ ├── Locking/ # LockHandler, Lock types, LockState
+│ ├── Effects/ # Zone effects, effect handler
+│ ├── DamageProcessors/ # DamageProcessor, DamageTakenEventArgs
+│ ├── Movements/ # Movement, PathMovement, RandomMovement, WaypointMovement
+│ ├── Teleporting/ # Teleport strategies, spark teleport
+│ ├── Scanning/ # Scanning modules, ammos, results
+│ ├── Beams/ # Visual beam effects
+│ ├── Decors/ # Decorative objects
+│ ├── Environments/ # Environmental conditions
+│ ├── Finders/ # Position finders, unit finders
+│ ├── Helpers/ # Zone utility helpers
+│ ├── Artifacts/
+│ ├── Blobs/
+│ ├── CombatLogs/
+│ ├── Eggs/
+│ ├── FieldEffectGenerators/
+│ ├── Gates/
+│ ├── LandMines/
+│ ├── LootContainers/
+│ ├── PlantTools/
+│ ├── ProximityDevices/
+│ ├── ProximityProbes/
+│ ├── PunchBags/
+│ ├── RemoteControl/
+│ ├── TerraformProjects/
+│ ├── Training/
+│ ├── Visibility/
+│ └── ZoneEntityRepositories/
+├── Services/
+│ ├── Sessions/ # ISession, Session, SessionManager
+│ ├── MarketEngine/ # Market orders, pricing, auto-orders
+│ ├── MissionEngine/ # Missions, targets, transport assignments, data cache
+│ ├── ProductionEngine/ # Crafting, research, calibration, reprocessing
+│ ├── Seasons/ # Season lifecycle and bonus system
+│ ├── EventServices/ # Pub/sub event bus, NPC spawn events
+│ ├── Standing/ # Faction/corp standing
+│ ├── Social/ # Friends, blocks, social list
+│ ├── Channels/ # Chat channels, chat commands
+│ ├── ExtensionService/ # Character skill extensions
+│ ├── Insurance/ # Robot insurance
+│ ├── ItemShop/ # In-game item shop
+│ ├── Looting/ # Loot tables, loot service
+│ ├── Mail/ # In-game mail
+│ ├── HighScores/ # Leaderboard tracking
+│ ├── Relics/ # Relic spawning and management
+│ ├── RiftSystem/ # Rift spawning
+│ ├── Sparks/ # Spark teleport system
+│ ├── Strongholds/ # Stronghold zone player state
+│ ├── TechTree/ # Tech tree progression
+│ ├── Trading/ # Player-to-player trade
+│ ├── Steam/ # Steam authentication
+│ ├── GameTime/ # Server time utilities
+│ ├── Relay/ # Relay server info
+│ ├── Artifacts/
+│ └── Weather/
+├── GenXY/ # Wire protocol: GenxyReader, GenxyWriter, GenxyConverter
+├── Network/ # TCP connections, encryption (RSA/RC4)
+├── Host/ # HostStateService, HostState, IRequestHandler, RequestHandlerFactory
+├── Data/ # Db static factory, DbQuery fluent builder, TransactionExtensions
+├── StateMachines/ # StackFSM, FiniteStateMachine, IState
+├── Threading/ # Process, ProcessManager, AsyncProcess, ImmutableInterlocked helpers
+├── Timers/ # IntervalTimer, various timer types
+├── Collections/ # Spatial collections
+├── Groups/ # Corporations, Gangs, Alliances
+├── Accounting/ # Account, Character, wallet, transaction logging
+├── Containers/ # Container entities, system containers
+├── Deployers/ # Item/entity deployers
+├── Common/ # Loggers, miscellaneous utilities
+├── IO/ # File system abstraction
+├── Log/ # Logger facade, ILogger
+├── Wallets/ # Wallet abstraction
+└── Commands.cs # Static registry of all ~200 commands
+```
+
+## Source Organisation — `src/Perpetuum.Bootstrapper/`
+
+```
+src/Perpetuum.Bootstrapper/
+├── PerpetuumBootstrapper.cs # Top-level init/start/stop; wires all modules
+├── Modules/
+│ ├── AutoActivatedTypesModule.cs
+│ ├── ChannelTypesModule.cs
+│ ├── CommandsModule.cs # Registers all Commands as keyed instances
+│ ├── EffectsModule.cs
+│ ├── EntitiesModule.cs # Entity system, all entity types, factories
+│ ├── IntrusionsModule.cs
+│ ├── LoggersModule.cs
+│ ├── MissionsModule.cs
+│ ├── MtProductsModule.cs
+│ ├── NpcsModule.cs # NPC flocks, presences, reinforcements, SAP
+│ ├── PbsModule.cs
+│ ├── RelicsModule.cs
+│ ├── RequestHandlersModule.cs # Maps every IRequest command to a handler class
+│ ├── RiftsModule.cs
+│ ├── RobotTemplatesModule.cs
+│ ├── SeasonModule.cs
+│ ├── TerrainsModule.cs
+│ ├── ZoneRequestHandlersModule.cs # Maps every IZoneRequest command to a handler class
+│ └── ZonesModule.cs # Zone types, zone services, session factory
+├── ContainerBuilderExtensions.cs
+├── EntityAggregateServices.cs
+├── RobotTemplateServices.cs
+└── TeleportStrategyFactories.cs
+```
+
+## Source Organisation — `src/Perpetuum.RequestHandlers/`
+
+```
+src/Perpetuum.RequestHandlers/
+├── AdminTools/
+├── Characters/
+├── Corporations/
+│ └── YellowPages/
+├── Extensions/
+├── FittingPreset/
+├── Gangs/
+├── Intrusion/
+├── Mails/
+├── Markets/
+├── Production/
+├── RobotTemplates/
+├── Socials/
+├── Sparks/
+├── Standings/
+├── TechTree/
+├── Trades/
+├── TransportAssignments/
+└── Zone/ # Zone-scoped handlers (movement, combat, terrain, etc.)
+```
+597 `.cs` files total across all subdirectories.
+
+## Key Files
+
+**Entry Points:**
+- `src/Perpetuum.Server/Program.cs` — console entry; parses `--GameRoot`, calls `bootstrapper.Init/Start`
+- `src/Perpetuum.ServerService2/` — Windows service wrapper for the same bootstrapper
+
+**Bootstrap:**
+- `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs` — `Init(gameRoot)` builds the DI container,
+ loads DB config, wires static service locators, initialises mission/market caches
+
+**Command Registry:**
+- `src/Perpetuum/Commands.cs` — every playable command defined as a `public static readonly Command`
+
+**Zone Root:**
+- `src/Perpetuum/Zones/Zone.cs` — abstract zone; subclassed by PveZone/PvpZone/StrongHoldZone/TrainingZone
+- `src/Perpetuum/Zones/ZoneConfiguration.cs` — reads zone rows from DB; assigns listener ports
+
+**Entity Root:**
+- `src/Perpetuum/EntityFramework/Entity.cs`
+- `src/Perpetuum/EntityFramework/EntityDefault.cs`
+
+**Data Access:**
+- `src/Perpetuum/Data/Db.cs` — `Db.Query()` fluent builder
+- `src/Perpetuum/Data/TransactionExtensions.cs` — `OnCommited` hook
+
+**Wire Protocol:**
+- `src/Perpetuum/GenXY/GenxyReader.cs`
+- `src/Perpetuum/GenXY/GenxyWriter.cs`
+- `src/Perpetuum/GenXY/GenxyConverter.cs`
+
+**Configuration:**
+- `src/Perpetuum.ServerService2/appsettings.json` — sets `GameRoot` path
+- `perpetuum.ini` (inside GameRoot) — SQL connection string, ports, zone config, feature flags
+
+## Module Organisation (Autofac)
+
+Modules registered in `PerpetuumBootstrapper.InitContainer()`:
+
+| Module | Registers |
+|--------|-----------|
+| `CommandsModule` | All `Command` instances keyed by uppercase text |
+| `RequestHandlersModule` | ~200 `IRequestHandler` mappings |
+| `ZoneRequestHandlersModule` | Zone-scoped `IRequestHandler` mappings |
+| `EntitiesModule` | Entity system; all entity/item/module/robot/NPC concrete types |
+| `ZonesModule` | Zone types, zone services (terrain, weather, beams, etc.), ZoneSession factory |
+| `NpcsModule` | Flock configs, presence types, reinforcements, SAP attackers |
+| `MissionsModule` | MissionDataCache, MissionProcessor, mission structures |
+| `TerrainsModule` | Terrain layers, material readers, plant rules |
+| `PbsModule` | PBS object types and handlers |
+| `RelicsModule` | Relic manager and repository |
+| `RiftsModule` | Rift manager and custom rift config |
+| `IntrusionsModule` | SAP types, intrusion site handlers |
+| `EffectsModule` | Effect types and handlers |
+| `RobotTemplatesModule` | Robot template loading and relations |
+| `SeasonModule` | SeasonService, SeasonRepository |
+| `ChannelTypesModule` | Chat channel type registrations |
+| `MtProductsModule` | Market/trade product registrations |
+| `LoggersModule` | Logger implementations |
+| `AutoActivatedTypesModule` | Types that must be instantiated at startup |
+
+Additional registrations in `PerpetuumBootstrapper.InitContainer()` and `InitRelayManager()`
+cover: Sessions, Corporations, Gangs, Market, Production, Standing, Social, Insurance,
+LootService, TechTree, Steam, Sparks, Wallets, TradeService, and more.
+
+## Naming Conventions
+
+**Files:**
+- One class per file; file name matches class name exactly
+- Partial classes use `ClassName.PartName.cs` (e.g., `ActiveModule.States.cs`, `Zone.cs` + `ZoneExtensions.Gang.cs`)
+- Interfaces prefixed with `I` (`IZone`, `IRequestHandler`, `ISession`)
+- Handlers named `.cs` matching the command text (PascalCase)
+
+**Directories:**
+- Feature-named subdirectories within each project layer
+- Zone sub-features grouped under `src/Perpetuum/Zones//`
+- Services grouped under `src/Perpetuum/Services//`
+
+## Where to Add New Code
+
+**New game command:**
+1. Define the `Command` in `src/Perpetuum/Commands.cs`
+2. Create handler class in `src/Perpetuum.RequestHandlers//` implementing
+ `IRequestHandler` or `IRequestHandler`
+3. Register in `src/Perpetuum.Bootstrapper/Modules/RequestHandlersModule.cs` or
+ `ZoneRequestHandlersModule.cs`
+
+**New service:**
+1. Implement in `src/Perpetuum/Services//`
+2. Register in the appropriate Autofac module in `src/Perpetuum.Bootstrapper/Modules/`,
+ or add registration directly in `PerpetuumBootstrapper.InitContainer()`
+3. If the service needs ticking, add to `IProcessManager` via
+ `.ToAsync().AsTimed(interval)` pattern in the `OnActivated` callback
+
+**New zone feature:**
+1. Add logic in `src/Perpetuum/Zones//`
+2. Add the interface/property to `IZone` and implement in `Zone.cs` if it is
+ zone-wide; otherwise inject via ZoneSession or unit
+
+**New entity type:**
+1. Subclass appropriate base (`Item`, `Unit`, `Robot`) in `src/Perpetuum/`
+2. Register concrete type in `EntitiesModule.cs` via `builder.RegisterType()`
+3. Add `CategoryFlags` or `DefinitionNames` entry in `src/Perpetuum.ExportedTypes/`
+
+**New module type:**
+1. Subclass `Module` or `ActiveModule` in `src/Perpetuum/Modules/`
+2. Register in `EntitiesModule.cs`
+
+## Special Directories
+
+**`docs/db_structure/`:**
+- Authoritative source for all DB schema, stored procedures, functions, views
+- Must be consulted before writing any SQL, query, or DB-touching code
+- Not generated at build time; maintained manually
+
+**`src/Perpetuum.AdminTool/`:**
+- Standalone WPF application for server admin tasks
+- Has its own data access layer under `src/Perpetuum.AdminTool/Data/`
+- Does not share the bootstrapper; connects directly to the DB
+
+**`bin/x64/Release/net8.0/`:**
+- CI build output directory
+- Generated, not committed
+
+---
+
+*Structure analysis: 2026-05-11*
diff --git a/docs/codebase/TESTING.md b/docs/codebase/TESTING.md
new file mode 100644
index 0000000..4600fe6
--- /dev/null
+++ b/docs/codebase/TESTING.md
@@ -0,0 +1,82 @@
+# Testing
+
+**Analysis Date:** 2026-05-11
+
+## Current State
+
+**No automated test suite exists in this repository.**
+
+There is no xUnit, NUnit, MSTest, or any other test framework. The CI pipeline (`.github/workflows/dotnet.yml`) runs only `dotnet build` and `dotnet restore` — no `dotnet test` step exists. There are no `*.Tests` projects, no test discovery configuration, and no code coverage tooling.
+
+## Test Infrastructure
+
+None. There is no test runner, no assertion library, no mock framework, and no test helpers.
+
+Files with "Test" in their name are not unit tests — they are **in-game admin commands** that exercise game logic against a live database while the server is running:
+
+| File | What it does |
+|------|-------------|
+| `src/Perpetuum.RequestHandlers/Extensions/ExtensionTest.cs` | Triggers extension-point grant and measures SQL execution time |
+| `src/Perpetuum.RequestHandlers/Zone/ZonePBSTest.cs` | Resets PBS highway bits on a live zone terrain |
+| `src/Perpetuum.RequestHandlers/Zone/ZoneTerraformTest.cs` | In-game terraform operation test |
+| `src/Perpetuum.RequestHandlers/Missions/MissionResolveTest.cs` | Admin command that invokes `MissionResolveTester` against live DB |
+| `src/Perpetuum/Services/MissionEngine/MissionResolveTester.cs` | Parallel mission resolve batch-runner; writes results to `missiontolocation` and `missiontargetslog` tables |
+| `src/Perpetuum/Services/MissionEngine/OneLocationTest.cs` | Single-location mission resolve helper used by `MissionResolveTester` |
+
+These are dispatched exactly like player commands (via `IRequestHandler`) with `AccessLevel.admin` and require a live game server with a connected SQL Server database. They are not isolated or repeatable without the full runtime.
+
+## Manual Testing
+
+The team tests by running the server locally against a configured `GameRoot`:
+
+```bash
+cd src/Perpetuum.Server
+dotnet run -- --GameRoot "E:\PerpetuumServer2\data"
+```
+
+Manual verification involves:
+- Connecting a game client to the server
+- Exercising features through normal gameplay or admin commands
+- Sending admin commands (e.g. `MissionResolveTest`, `ExtensionTest`) via client or a tool
+- Inspecting server console log output (`Logger.Info/Warning/Error`) and file logs under `logs/`
+- Querying the SQL Server database directly to verify data state
+
+The `Perpetuum.AdminTool` project (`src/Perpetuum.AdminTool/`) provides a GUI tool for administrative operations including the Seasons Admin Tool.
+
+## CI Pipeline
+
+`.github/workflows/dotnet.yml` runs on pushes and pull requests to `develop` branch only:
+
+```yaml
+- dotnet restore
+- dotnet build src/Perpetuum.ServerService2/... --configuration Release -p:Platform=x64
+```
+
+A build artifact is uploaded on successful push. No tests are executed in CI.
+
+## Gaps
+
+**Everything is untested at the automated level.** Specific high-risk gaps:
+
+- **Entity system** (`src/Perpetuum/EntityFramework/`) — `Entity`, `EntityDefault`, `EntityDynamicProperties` have no isolation tests. These underpin all game objects.
+- **Module state machines** (`src/Perpetuum/Modules/ActiveModule.States.cs`) — state transitions for robot equipment are tested only by playing the game.
+- **Guard/validation extensions** (`src/Perpetuum/Guard.cs`) — the `ThrowIf*` extension methods are used pervasively but never unit-tested.
+- **Database query layer** (`src/Perpetuum/Data/DbQuery.cs`, `Db.cs`) — no integration test harness for SQL query correctness.
+- **Request handlers** (`src/Perpetuum.RequestHandlers/`) — 200+ handler classes have no test doubles or mock session/request infrastructure.
+- **Concurrent/threading code** — `ProcessManager`, `MessageSender`, `TcpConnection` use `ThreadPool` and `Task.Run` patterns that are notoriously difficult to test without a harness.
+- **Season service logic** (`src/Perpetuum/Services/Seasons/SeasonService.cs`) — tier grant, objective completion, leaderboard delivery, intro mail idempotency, and end-of-season processing are exercised only via live play.
+- **Mission engine** (`src/Perpetuum/Services/MissionEngine/`) — the most complex subsystem; the existing `MissionResolveTester` exercises resolve logic but requires a live DB and has no assertions — it logs results to tables for human inspection.
+
+## Adding Tests (If Introduced)
+
+To add automated tests, the recommended path would be:
+
+1. Create a `Perpetuum.Tests` project (xUnit recommended for .NET 8)
+2. Add project reference to `Perpetuum` core library
+3. The main obstacle is the pervasive use of static service locators (`Entity.Services`, `EntityDefault.Reader`, `Db.DbQueryFactory`, `Logger.Current`) — these would need to be initialized or replaced with test doubles before any entity or database code can run in isolation
+4. `Guard.cs` extension methods and `ValueTypeExtensions.cs` are pure functions with no dependencies — good first candidates for unit tests
+5. `SeasonRepository` methods are testable with an in-memory or test SQL database since they only use `Db.Query()` which is factory-injected
+
+---
+
+*Testing analysis: 2026-05-11*
diff --git a/docs/db_structure/data_types/IntList.sql b/docs/db_structure/data_types/IntList.sql
new file mode 100644
index 0000000..faa559c
--- /dev/null
+++ b/docs/db_structure/data_types/IntList.sql
@@ -0,0 +1,5 @@
+/****** Object: UserDefinedTableType [dbo].[IntList] Script Date: 10.05.2026 7:30:19 ******/
+CREATE TYPE [dbo].[IntList] AS TABLE(
+ [idval] [int] NOT NULL
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/data_types/integer_list_tbltype.sql b/docs/db_structure/data_types/integer_list_tbltype.sql
new file mode 100644
index 0000000..5cdd066
--- /dev/null
+++ b/docs/db_structure/data_types/integer_list_tbltype.sql
@@ -0,0 +1,9 @@
+/****** Object: UserDefinedTableType [dbo].[integer_list_tbltype] Script Date: 10.05.2026 7:29:35 ******/
+CREATE TYPE [dbo].[integer_list_tbltype] AS TABLE(
+ [n] [int] NOT NULL,
+ PRIMARY KEY CLUSTERED
+(
+ [n] ASC
+)WITH (IGNORE_DUP_KEY = OFF)
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/data_types/long_list_tbltype.sql b/docs/db_structure/data_types/long_list_tbltype.sql
new file mode 100644
index 0000000..c4e38f5
--- /dev/null
+++ b/docs/db_structure/data_types/long_list_tbltype.sql
@@ -0,0 +1,9 @@
+/****** Object: UserDefinedTableType [dbo].[long_list_tbltype] Script Date: 10.05.2026 7:31:05 ******/
+CREATE TYPE [dbo].[long_list_tbltype] AS TABLE(
+ [n] [bigint] NOT NULL,
+ PRIMARY KEY CLUSTERED
+(
+ [n] ASC
+)WITH (IGNORE_DUP_KEY = OFF)
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/database_schema_documentation.md b/docs/db_structure/database_schema_documentation.md
new file mode 100644
index 0000000..c5056e8
--- /dev/null
+++ b/docs/db_structure/database_schema_documentation.md
@@ -0,0 +1,7203 @@
+# Database Schema Documentation
+
+Generated from DBML structure.
+
+## Table of Contents
+
+- [accountcampaignitems](#accountcampaignitems)
+- [accountcreditqueue](#accountcreditqueue)
+- [accountextensionbought](#accountextensionbought)
+- [accountextensionpenalty](#accountextensionpenalty)
+- [entitydefaults](#entitydefaults)
+- [extensioncategories](#extensioncategories)
+- [aggregatefields](#aggregatefields)
+- [extensions](#extensions)
+- [accountextensionspent](#accountextensionspent)
+- [accountonlinetime](#accountonlinetime)
+- [packages](#packages)
+- [accounts](#accounts)
+- [accountpremiumpackages](#accountpremiumpackages)
+- [accountredeemableitems](#accountredeemableitems)
+- [accounttransactionlog](#accounttransactionlog)
+- [adminCommandLog](#admincommandlog)
+- [aggregatemodifiers](#aggregatemodifiers)
+- [aggregatevalues](#aggregatevalues)
+- [corporations](#corporations)
+- [alliances](#alliances)
+- [alliancemembers](#alliancemembers)
+- [artifactloot](#artifactloot)
+- [artifacts](#artifacts)
+- [artifactspawninfo](#artifactspawninfo)
+- [artifacttypes](#artifacttypes)
+- [attributeFlags](#attributeflags)
+- [automarket_unbought_resources](#automarket-unbought-resources)
+- [automarket_unsold_leftovers](#automarket-unsold-leftovers)
+- [beams](#beams)
+- [beamassignment](#beamassignment)
+- [bulletinentries](#bulletinentries)
+- [bulletins](#bulletins)
+- [calibrationdefaults](#calibrationdefaults)
+- [calibrationtemplateitems](#calibrationtemplateitems)
+- [campaigns](#campaigns)
+- [campaigngoodiepacks](#campaigngoodiepacks)
+- [categoryFlags](#categoryflags)
+- [categorygroups](#categorygroups)
+- [categorygroupsnames](#categorygroupsnames)
+- [centralbanklog](#centralbanklog)
+- [centralbanktransactions](#centralbanktransactions)
+- [channelbans](#channelbans)
+- [channelmembers](#channelmembers)
+- [channels](#channels)
+- [characterextensions](#characterextensions)
+- [characterhighscore](#characterhighscore)
+- [characterkillreports](#characterkillreports)
+- [charactermessages](#charactermessages)
+- [characternickhistory](#characternickhistory)
+- [characternotes](#characternotes)
+- [characternpcdeath](#characternpcdeath)
+- [characterreimburselog](#characterreimburselog)
+- [characters](#characters)
+- [charactersettings](#charactersettings)
+- [charactersocial](#charactersocial)
+- [charactersparks](#charactersparks)
+- [charactersparkteleports](#charactersparkteleports)
+- [charactertransactions](#charactertransactions)
+- [chassisbonus](#chassisbonus)
+- [cmails](#cmails)
+- [combatlog](#combatlog)
+- [components](#components)
+- [connectedips](#connectedips)
+- [containerlog](#containerlog)
+- [corporationApplication](#corporationapplication)
+- [corporationceotakeover](#corporationceotakeover)
+- [corporationdocumentconfig](#corporationdocumentconfig)
+- [corporationdocumentregistration](#corporationdocumentregistration)
+- [corporationdocuments](#corporationdocuments)
+- [corporationhistory](#corporationhistory)
+- [corporationleave](#corporationleave)
+- [corporationlog](#corporationlog)
+- [corporationmembers](#corporationmembers)
+- [corporationnamehistory](#corporationnamehistory)
+- [corporationrolehistory](#corporationrolehistory)
+- [corporationtransactions](#corporationtransactions)
+- [countries](#countries)
+- [cw_race](#cw-race)
+- [cw_school](#cw-school)
+- [cw_corporation](#cw-corporation)
+- [cw_corporation_extension](#cw-corporation-extension)
+- [cw_major](#cw-major)
+- [cw_major_extension](#cw-major-extension)
+- [cw_race_extension](#cw-race-extension)
+- [cw_school_extension](#cw-school-extension)
+- [cw_spark](#cw-spark)
+- [cw_spark_extension](#cw-spark-extension)
+- [decorcategories](#decorcategories)
+- [decor](#decor)
+- [defaultfieldscalculation](#defaultfieldscalculation)
+- [definitionconfig](#definitionconfig)
+- [definitionconfigunits](#definitionconfigunits)
+- [dynamiccalibrationtemplates](#dynamiccalibrationtemplates)
+- [effectcategories](#effectcategories)
+- [effectdefaultmodifiers](#effectdefaultmodifiers)
+- [effects](#effects)
+- [enablerextensions](#enablerextensions)
+- [entities](#entities)
+- [entitystorage](#entitystorage)
+- [entitytemplates](#entitytemplates)
+- [entitytrash](#entitytrash)
+- [environmentdescription](#environmentdescription)
+- [environmentdescriptionstaging](#environmentdescriptionstaging)
+- [epforactivitylog](#epforactivitylog)
+- [extensionpointpenalty](#extensionpointpenalty)
+- [extensionpoints](#extensionpoints)
+- [extensionpointworklog](#extensionpointworklog)
+- [extensionprerequire](#extensionprerequire)
+- [extensionremovelog](#extensionremovelog)
+- [extensionsubscription](#extensionsubscription)
+- [facilitymap](#facilitymap)
+- [gameglobals](#gameglobals)
+- [gang](#gang)
+- [gangmembers](#gangmembers)
+- [giftloots](#giftloots)
+- [hardwareinfo](#hardwareinfo)
+- [harvestlog](#harvestlog)
+- [hostconfig](#hostconfig)
+- [icetracker](#icetracker)
+- [insurance](#insurance)
+- [insuranceprices](#insuranceprices)
+- [intrusiondockingrightslog](#intrusiondockingrightslog)
+- [intrusioneffectlog](#intrusioneffectlog)
+- [intrusionloot](#intrusionloot)
+- [intrusionproductionlog](#intrusionproductionlog)
+- [intrusionproductionstack](#intrusionproductionstack)
+- [intrusionsapdeploylog](#intrusionsapdeploylog)
+- [intrusionsaps](#intrusionsaps)
+- [intrusionsitelog](#intrusionsitelog)
+- [intrusionsitemessagelog](#intrusionsitemessagelog)
+- [intrusionsites](#intrusionsites)
+- [intrusionsitestabilitythreshold](#intrusionsitestabilitythreshold)
+- [itemcreation](#itemcreation)
+- [itemprices](#itemprices)
+- [itemresearchlevels](#itemresearchlevels)
+- [itemscore](#itemscore)
+- [itemshop](#itemshop)
+- [itemshoppresets](#itemshoppresets)
+- [itemshoplocations](#itemshoplocations)
+- [killreports](#killreports)
+- [locktest](#locktest)
+- [lootitems](#lootitems)
+- [lotteryitemweights](#lotteryitemweights)
+- [market_orders_configuration](#market-orders-configuration)
+- [marketaverageprices](#marketaverageprices)
+- [marketaveragesbycomponent](#marketaveragesbycomponent)
+- [marketitems](#marketitems)
+- [markettaxlog](#markettaxlog)
+- [mineralconfigs](#mineralconfigs)
+- [mineralnodes](#mineralnodes)
+- [minerals](#minerals)
+- [mineralscan](#mineralscan)
+- [mininglog](#mininglog)
+- [missionagents](#missionagents)
+- [missionbonus](#missionbonus)
+- [missionconstants](#missionconstants)
+- [zones](#zones)
+- [teleportdescriptions](#teleportdescriptions)
+- [missiontypes](#missiontypes)
+- [missionissuer](#missionissuer)
+- [missions](#missions)
+- [missionenterpoints](#missionenterpoints)
+- [missiongrind](#missiongrind)
+- [missionlocations](#missionlocations)
+- [missionlog](#missionlog)
+- [missionparticipants](#missionparticipants)
+- [missionpayoutlog](#missionpayoutlog)
+- [missionrequiredextensions](#missionrequiredextensions)
+- [missionrequiredmissions](#missionrequiredmissions)
+- [missionrequiredstanding](#missionrequiredstanding)
+- [missionrewards](#missionrewards)
+- [missionspotinfo](#missionspotinfo)
+- [missionstandingchange](#missionstandingchange)
+- [missionstartitem](#missionstartitem)
+- [missiontargettypes](#missiontargettypes)
+- [missiontargets](#missiontargets)
+- [missiontargetsarchive](#missiontargetsarchive)
+- [missiontargetslog](#missiontargetslog)
+- [missiontoagent](#missiontoagent)
+- [missiontolocation](#missiontolocation)
+- [modulepropertymodifiers](#modulepropertymodifiers)
+- [mtproductprices](#mtproductprices)
+- [newscategories](#newscategories)
+- [news](#news)
+- [npcbossinfo](#npcbossinfo)
+- [npccontaineritems](#npccontaineritems)
+- [npcescalactions](#npcescalactions)
+- [npcspawn](#npcspawn)
+- [npcpresence](#npcpresence)
+- [npcflock](#npcflock)
+- [npcflockloot](#npcflockloot)
+- [npcinterzonegroup](#npcinterzonegroup)
+- [npckills](#npckills)
+- [npcloot](#npcloot)
+- [npcpoolpresets](#npcpoolpresets)
+- [npcpoolpresetvalues](#npcpoolpresetvalues)
+- [npcrandomflockpool](#npcrandomflockpool)
+- [npcreinforcements](#npcreinforcements)
+- [npcreinforcementtypes](#npcreinforcementtypes)
+- [npcsafespawnpoints](#npcsafespawnpoints)
+- [npcSpecialTypes](#npcspecialtypes)
+- [nspools](#nspools)
+- [nspoolmembers](#nspoolmembers)
+- [nspoolrelation](#nspoolrelation)
+- [nstemplates](#nstemplates)
+- [opp_reimburselog](#opp-reimburselog)
+- [ownerincome](#ownerincome)
+- [packageitems](#packageitems)
+- [passablemappoints](#passablemappoints)
+- [paymentproducts](#paymentproducts)
+- [paypal_transactions_history](#paypal-transactions-history)
+- [pbsconnections](#pbsconnections)
+- [pbslog](#pbslog)
+- [pbsregisteredmembers](#pbsregisteredmembers)
+- [pbsreimburse](#pbsreimburse)
+- [pbstrash](#pbstrash)
+- [plantdamagetype](#plantdamagetype)
+- [plantrules](#plantrules)
+- [plasma_gathered](#plasma-gathered)
+- [plasma_gathered_daily](#plasma-gathered-daily)
+- [plasma_sold](#plasma-sold)
+- [polls](#polls)
+- [pollanswers](#pollanswers)
+- [pollchoices](#pollchoices)
+- [premadechatmessage](#premadechatmessage)
+- [premademail](#premademail)
+- [productioncost](#productioncost)
+- [productiondecalibration](#productiondecalibration)
+- [productionduration](#productionduration)
+- [productionlines](#productionlines)
+- [productionlog](#productionlog)
+- [prototypes](#prototypes)
+- [rarematerials](#rarematerials)
+- [raw_material_prices](#raw-material-prices)
+- [reimbursementlog](#reimbursementlog)
+- [relays](#relays)
+- [relicloot](#relicloot)
+- [relicspawninfo](#relicspawninfo)
+- [relictypes](#relictypes)
+- [reliczoneconfig](#reliczoneconfig)
+- [resource_market_prices](#resource-market-prices)
+- [resources_gathered](#resources-gathered)
+- [resources_gathered_daily](#resources-gathered-daily)
+- [riftconfigs](#riftconfigs)
+- [riftdestinations](#riftdestinations)
+- [robotassembler](#robotassembler)
+- [robotfittingpresets](#robotfittingpresets)
+- [robotsavedeffects](#robotsavedeffects)
+- [robotsetup](#robotsetup)
+- [robottemplates](#robottemplates)
+- [robottemplaterelation](#robottemplaterelation)
+- [runningproduction](#runningproduction)
+- [runningproductionreserveditem](#runningproductionreserveditem)
+- [savedeffects](#savedeffects)
+- [season_activity_rates](#season-activity-rates)
+- [season_character_points](#season-character-points)
+- [season_leaderboard_rewards](#season-leaderboard-rewards)
+- [season_objective_progress](#season-objective-progress)
+- [season_objectives](#season-objectives)
+- [season_tier_claims](#season-tier-claims)
+- [season_tiers](#season-tiers)
+- [seasons](#seasons)
+- [serverinfo](#serverinfo)
+- [settings](#settings)
+- [siegeitems](#siegeitems)
+- [slotFlags](#slotflags)
+- [sparks](#sparks)
+- [sparkextensions](#sparkextensions)
+- [standinglog](#standinglog)
+- [standings](#standings)
+- [steamkeys](#steamkeys)
+- [steamkeyscomp](#steamkeyscomp)
+- [storecategories](#storecategories)
+- [storeitems](#storeitems)
+- [strongholdexitconfig](#strongholdexitconfig)
+- [techline](#techline)
+- [techlineincrement](#techlineincrement)
+- [techlinemember](#techlinemember)
+- [techtree](#techtree)
+- [techtreegroups](#techtreegroups)
+- [techtreelog](#techtreelog)
+- [techtreenodeprices](#techtreenodeprices)
+- [techtreepoints](#techtreepoints)
+- [techtreepointtypes](#techtreepointtypes)
+- [techtreeunlockednodes](#techtreeunlockednodes)
+- [terraformprojectregistration](#terraformprojectregistration)
+- [terraformprojects](#terraformprojects)
+- [tiertypes](#tiertypes)
+- [traceips](#traceips)
+- [traceroutelog](#traceroutelog)
+- [trainingartifacts](#trainingartifacts)
+- [trainingrewards](#trainingrewards)
+- [transactiontypes](#transactiontypes)
+- [transportassignments](#transportassignments)
+- [transportassignmentslog](#transportassignmentslog)
+- [transportassignmenttimes](#transportassignmenttimes)
+- [usercount](#usercount)
+- [vendorpresets](#vendorpresets)
+- [vendorpresetvalues](#vendorpresetvalues)
+- [vendors](#vendors)
+- [votes](#votes)
+- [voteentries](#voteentries)
+- [yellowpages](#yellowpages)
+- [zoneeffects](#zoneeffects)
+- [zoneentities](#zoneentities)
+- [zoneriftsconfig](#zoneriftsconfig)
+- [zonesectors](#zonesectors)
+- [zoneteleportdevicemap](#zoneteleportdevicemap)
+- [zoneuserentities](#zoneuserentities)
+
+---
+
+## accountcampaignitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountid` | `int [not null]` |
+| `campaignid` | `int [not null]` |
+| `redeemed` | `bit [not null, default: 0]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `redeemdate` | `datetime` |
+
+---
+
+## accountcreditqueue
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `credit` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+
+### Indexes
+
+- `id [pk, name: "PK_accountcreditqueue"]`
+
+---
+
+## accountextensionbought
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountid` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `points` | `int [not null]` |
+| `packagetype` | `int [not null, default: 0]` |
+
+---
+
+## accountextensionpenalty
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `points` | `int [not null, default: 0]` |
+| `forever` | `bit [not null, default: 0]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## entitydefaults
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `"int IDENTITY(1,1)" [not null]` |
+| `definitionname` | `varchar(100) [not null]` |
+| `quantity` | `int [not null, default: 1]` |
+| `attributeflags` | `bigint [not null, default: 0]` |
+| `categoryflags` | `bigint [not null]` |
+| `options` | `varchar(MAX)` |
+| `note` | `nvarchar(2048)` |
+| `enabled` | `bit [not null, default: 1]` |
+| `volume` | `float [default: 0]` |
+| `mass` | `float [default: 0]` |
+| `hidden` | `bit [not null, default: 0]` |
+| `health` | `float [not null, default: 100]` |
+| `descriptiontoken` | `nvarchar(100)` |
+| `purchasable` | `bit [not null, default: 1]` |
+| `tiertype` | `int` |
+| `tierlevel` | `int` |
+
+### Indexes
+
+- `definition [pk, name: "PK_entitydefaults"]`
+- `definitionname [unique, name: "IX_entitydefaults_name"]`
+
+### Relations
+
+- `definition` → `aggregatevalues.definition`
+- `definition` → `beamassignment.definition`
+- `definition` → `chassisbonus.definition`
+- `definition` → `components.definition`
+- `definition` → `components.componentdefinition`
+- `definition` → `decor.definition`
+- `definition` → `definitionconfig.definition`
+- `definition` → `dynamiccalibrationtemplates.definition`
+- `definition` → `dynamiccalibrationtemplates.targetdefinition`
+- `definition` → `enablerextensions.definition`
+- `definition` → `environmentdescription.definition`
+- `definition` → `environmentdescriptionstaging.definition`
+- `definition` → `giftloots.definition`
+- `definition` → `insuranceprices.definition`
+- `definition` → `intrusionloot.itemdefinition`
+- `definition` → `intrusionloot.sitedefinition`
+- `definition` → `intrusionloot.sapdefinition`
+- `definition` → `itemprices.definition`
+- `definition` → `itemresearchlevels.definition`
+- `definition` → `itemresearchlevels.calibrationprogram`
+- `definition` → `itemshop.targetdefinition`
+- `definition` → `missionrewards.definition`
+- `definition` → `missionstartitem.definition`
+- `definition` → `missiontargets.definition`
+- `definition` → `npccontaineritems.definition`
+- `definition` → `npccontaineritems.lootdefinition`
+- `definition` → `npcflock.definition`
+- `definition` → `npcflockloot.lootdefinition`
+- `definition` → `npcloot.definition`
+- `definition` → `npcloot.lootdefinition`
+- `definition` → `nspoolmembers.definition`
+- `definition` → `nstemplates.definition`
+- `definition` → `plantdamagetype.definition`
+- `definition` → `prototypes.definition`
+- `definition` → `prototypes.prototype`
+- `definition` → `robotsetup.robotshell`
+- `definition` → `robotsetup.head`
+- `definition` → `robotsetup.chassis`
+- `definition` → `robotsetup.leg`
+- `definition` → `robotsetup.container`
+- `definition` → `robotsetup.hybridshell`
+- `definition` → `robottemplaterelation.definition`
+- `definition` → `siegeitems.definition`
+- `definition` → `storeitems.definition`
+- `definition` → `techlineincrement.definition`
+- `definition` → `techlinemember.definition`
+- `definition` → `vendorpresetvalues.definition`
+
+---
+
+## extensioncategories
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `extensioncategoryid` | `int [not null]` |
+| `categoryname` | `varchar(50) [not null]` |
+| `hidden` | `bit [not null, default: 0]` |
+| `note` | `nvarchar(2048)` |
+
+### Relations
+
+- `extensioncategoryid` → `extensions.category`
+
+---
+
+## aggregatefields
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `nvarchar(100) [not null]` |
+| `formula` | `int [not null, default: 1]` |
+| `measurementunit` | `varchar(100)` |
+| `measurementmultiplier` | `float [not null, default: 1]` |
+| `measurementoffset` | `float [not null, default: 0]` |
+| `category` | `int [not null, default: 0]` |
+| `digits` | `int [not null, default: 0]` |
+| `moreisbetter` | `bit` |
+| `usedinconfig` | `bit` |
+| `note` | `nvarchar(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_aggregatefields"]`
+- `name [unique, name: "IX_aggregatefields"]`
+
+### Relations
+
+- `id` → `extensions.targetpropertyID`
+- `id` → `aggregatevalues.field`
+- `id` → `chassisbonus.targetpropertyID`
+- `id` → `effectdefaultmodifiers.field`
+- `id` → `nstemplates.field`
+
+---
+
+## extensions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `extensionid` | `int [not null]` |
+| `extensionname` | `varchar(128) [not null]` |
+| `category` | `int [not null]` |
+| `rank` | `int [not null]` |
+| `targetlearningattribute` | `varchar(50)` |
+| `learningattributeprimary` | `varchar(50) [not null]` |
+| `learningattributesecondary` | `varchar(50)` |
+| `bonus` | `float [not null]` |
+| `note` | `nvarchar(2048)` |
+| `price` | `int [not null, default: 103]` |
+| `active` | `bit [not null, default: 1]` |
+| `description` | `varchar(128)` |
+| `targetpropertyID` | `int` |
+| `effectenhancer` | `bit [not null, default: 0]` |
+| `hidden` | `bit [not null, default: 0]` |
+| `freezelimit` | `int` |
+
+### Indexes
+
+- `extensionname [unique, name: "IX_extensions_name"]`
+
+### Relations
+
+- Referenced by `aggregatefields.id`
+- Referenced by `extensioncategories.extensioncategoryid`
+- `extensionid` → `accountextensionspent.extensionid`
+- `extensionid` → `characterextensions.extensionid`
+- `extensionid` → `chassisbonus.extension`
+- `extensionid` → `cw_corporation_extension.extensionid`
+- `extensionid` → `cw_major_extension.extensionid`
+- `extensionid` → `cw_race_extension.extensionid`
+- `extensionid` → `cw_school_extension.extensionid`
+- `extensionid` → `cw_spark_extension.extensionid`
+- `extensionid` → `enablerextensions.extensionid`
+- `extensionid` → `extensionprerequire.extensionid`
+- `extensionid` → `extensionprerequire.requiredextension`
+- `extensionid` → `missionrequiredextensions.extensionid`
+- `extensionid` → `sparkextensions.extensionid`
+
+---
+
+## accountextensionspent
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountid` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `points` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `extensionlevel` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+
+### Relations
+
+- Referenced by `extensions.extensionid`
+
+---
+
+## accountonlinetime
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountid` | `int [not null]` |
+| `loggedin` | `datetime [not null, default: `getdate()`]` |
+| `loggedout` | `datetime` |
+| `ip` | `varchar(50) [not null]` |
+| `safelogout` | `bit [not null, default: 0]` |
+| `hwhash` | `varchar(50)` |
+| `istrial` | `bit [not null, default: 0]` |
+
+---
+
+## packages
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(64) [not null]` |
+| `note` | `nvarchar(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_premiumpackages"]`
+
+### Relations
+
+- `id` → `accountpremiumpackages.packageid`
+- `id` → `packageitems.packageid`
+
+---
+
+## accounts
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountID` | `"int IDENTITY(1,1)" [not null]` |
+| `email` | `varchar(50)` |
+| `password` | `varchar(100)` |
+| `firstName` | `nvarchar(50)` |
+| `lastName` | `nvarchar(50)` |
+| `born` | `smalldatetime` |
+| `state` | `int [not null, default: 1]` |
+| `accLevel` | `int [not null, default: 16777216]` |
+| `totalMinsOnline` | `int [not null, default: 0]` |
+| `lastLoggedIn` | `smalldatetime` |
+| `creation` | `smalldatetime [default: `getdate()`]` |
+| `clientType` | `tinyint [not null, default: 0]` |
+| `isLoggedIn` | `bit [not null, default: 0]` |
+| `bantime` | `smalldatetime [default: `getdate()`]` |
+| `banlength` | `int [not null, default: 120]` |
+| `bannote` | `nvarchar(512)` |
+| `emailConfirmed` | `bit [not null, default: 1]` |
+| `firstcharacter` | `datetime` |
+| `note` | `nvarchar(1024)` |
+| `steamID` | `varchar(20)` |
+| `twitchAuthToken` | `varchar(256)` |
+| `credit` | `int [not null, default: 0]` |
+| `isactive` | `bit [not null, default: 1]` |
+| `resetcount` | `int [not null, default: 0]` |
+| `wasreset` | `bit [not null, default: 0]` |
+| `validUntil` | `smalldatetime` |
+| `payingcustomer` | `bit [not null, default: 0]` |
+| `campaignid` | `varchar(512)` |
+
+### Indexes
+
+- `accountID [pk, name: "PK_accounts_aid"]`
+- `email [unique, name: "UK_accounts"]`
+
+### Relations
+
+- `accountID` → `accountpremiumpackages.accountid`
+
+---
+
+## accountpremiumpackages
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `packageid` | `int [not null]` |
+| `purchasetime` | `datetime [not null, default: `getdate()`]` |
+
+### Indexes
+
+- `id [pk, name: "PK_accountpremiumpackages"]`
+
+### Relations
+
+- Referenced by `accounts.accountID`
+- Referenced by `packages.id`
+
+---
+
+## accountredeemableitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null, default: 1]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `redeemed` | `datetime` |
+| `characterid` | `int` |
+| `wasredeemed` | `bit [not null, default: 0]` |
+| `packageid` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_accountredeemableitems"]`
+
+---
+
+## accounttransactionlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountId` | `nchar(10) [not null]` |
+| `transactionType` | `int [not null]` |
+| `definition` | `int` |
+| `quantity` | `int` |
+| `eid` | `bigint` |
+| `credit` | `int [not null, default: 0]` |
+| `creditChange` | `int [not null, default: 0]` |
+| `created` | `datetime [not null]` |
+
+---
+
+## adminCommandLog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `characterid` | `int [not null]` |
+| `accLevel` | `int [not null]` |
+| `message` | `nvarchar(255)` |
+
+---
+
+## aggregatemodifiers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `categoryflag` | `bigint [not null]` |
+| `basefield` | `int [not null]` |
+| `modifierfield` | `int [not null]` |
+
+---
+
+## aggregatevalues
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `field` | `int [not null]` |
+| `[value]` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_aggregatevalues"]`
+- `(definition, field) [unique, name: "IX_aggregatevalues"]`
+
+### Relations
+
+- Referenced by `aggregatefields.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## corporations
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `nick` | `varchar(6)` |
+| `wallet` | `float [not null, default: 0]` |
+| `taxrate` | `int [not null, default: 0]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `defaultcorp` | `bit [not null, default: 0]` |
+| `active` | `bit [not null, default: 1]` |
+| `founder` | `int` |
+| `publicprofile` | `nvarchar(MAX)` |
+| `privateprofile` | `nvarchar(MAX)` |
+| `color` | `int` |
+
+### Indexes
+
+- `eid [pk, name: "PK_corporation"]`
+
+### Relations
+
+- `eid` → `alliancemembers.corporationEID`
+- `eid` → `corporationApplication.corporationEID`
+- `eid` → `corporationhistory.corporationEID`
+- `eid` → `corporationmembers.corporationEID`
+- `eid` → `corporationrolehistory.corporationEID`
+- `eid` → `cw_corporation.corporationEID`
+- `eid` → `missionrequiredstanding.corporationeid`
+
+---
+
+## alliances
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `allianceEID` | `bigint [not null]` |
+| `name` | `nvarchar(50) [not null]` |
+| `nick` | `varchar(6) [not null]` |
+| `note` | `nvarchar(2048)` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `defaultAlliance` | `bit [not null, default: 0]` |
+| `active` | `bit [not null, default: 1]` |
+| `logoresource` | `varchar(50)` |
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `raceid` | `int` |
+
+### Indexes
+
+- `allianceEID [pk, name: "PK_alliances_eid"]`
+
+### Relations
+
+- `allianceEID` → `alliancemembers.allianceEID`
+
+---
+
+## alliancemembers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `allianceEID` | `bigint [not null]` |
+| `corporationEID` | `bigint [not null]` |
+
+### Relations
+
+- Referenced by `alliances.allianceEID`
+- Referenced by `corporations.eid`
+
+---
+
+## artifactloot
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `artifacttype` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `minquantity` | `int [not null, default: 1]` |
+| `maxquantity` | `int [not null]` |
+| `chance` | `float [not null]` |
+| `packed` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_artifactloot"]`
+
+---
+
+## artifacts
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `artifacttype` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `positionx` | `int [not null]` |
+| `positiony` | `int [not null]` |
+| `missionguid` | `uniqueidentifier` |
+| `created` | `datetime [default: `getdate()`]` |
+
+### Indexes
+
+- `id [pk, name: "PK_artifacts"]`
+
+---
+
+## artifactspawninfo
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `artifacttype` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `rate` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_artifactspawninfo"]`
+
+---
+
+## artifacttypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `goalrange` | `int [not null, default: 1]` |
+| `npcpresenceid` | `int` |
+| `persistent` | `bit [not null, default: 1]` |
+| `minimumloot` | `int [not null, default: 1]` |
+| `dynamic` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_artifacttypes"]`
+- `name [unique, name: "IX_artifacttypes_unique"]`
+
+---
+
+## attributeFlags
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `offset` | `int [not null]` |
+| `name` | `nvarchar(50) [not null]` |
+| `note` | `nvarchar(2048)` |
+
+### Indexes
+
+- `offset [unique, name: "IX_attributeFlags_offset"]`
+
+---
+
+## automarket_unbought_resources
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `itemdefinition` | `int [not null]` |
+| `quantity` | `bigint [not null]` |
+
+---
+
+## automarket_unsold_leftovers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `itemdefinition` | `int [not null]` |
+| `quantity` | `bigint [not null]` |
+
+---
+
+## beams
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `cycletime` | `int [not null]` |
+| `startdelay` | `int [not null, default: 0]` |
+| `description` | `varchar(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_beams"]`
+
+### Relations
+
+- `id` → `beamassignment.beam`
+
+---
+
+## beamassignment
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `beam` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_beamassignment"]`
+- `definition [unique, name: "IX_beamassignment"]`
+
+### Relations
+
+- Referenced by `beams.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## bulletinentries
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `entryID` | `"int IDENTITY(1,1)" [not null]` |
+| `bulletinID` | `int [not null]` |
+| `characterID` | `int [not null]` |
+| `entrytext` | `nvarchar(2000) [not null]` |
+| `entrydate` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## bulletins
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `bulletinID` | `"int IDENTITY(1,1)" [not null]` |
+| `groupEID` | `bigint [not null]` |
+| `title` | `nvarchar(256) [not null]` |
+| `startdate` | `datetime [not null, default: `getdate()`]` |
+| `startedby` | `int [not null]` |
+
+---
+
+## calibrationdefaults
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `materialefficiency` | `float [not null]` |
+| `timeefficiency` | `float [not null]` |
+
+### Indexes
+
+- `definition [pk, name: "PK_calibrationdefaults"]`
+
+---
+
+## calibrationtemplateitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int` |
+| `targetdefinition` | `int` |
+
+---
+
+## campaigns
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `campaigntoken` | `varchar(128) [not null]` |
+| `note` | `nvarchar(2048)` |
+
+### Indexes
+
+- `id [pk, name: "PK_campaigns"]`
+
+### Relations
+
+- `id` → `campaigngoodiepacks.campaignid`
+
+---
+
+## campaigngoodiepacks
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(512) [not null]` |
+| `description` | `varchar(512)` |
+| `campaignid` | `int [not null]` |
+| `credit` | `int` |
+| `ep` | `int` |
+| `faction` | `varchar(8)` |
+| `item0` | `int` |
+| `quantity0` | `int` |
+| `item1` | `int` |
+| `quantity1` | `int` |
+| `item2` | `int` |
+| `quantity2` | `int` |
+| `item3` | `int` |
+| `quantity3` | `int` |
+| `item4` | `int` |
+| `quantity4` | `int` |
+| `item5` | `int` |
+| `quantity5` | `int` |
+| `item6` | `int` |
+| `quantity6` | `int` |
+| `item7` | `int` |
+| `quantity7` | `int` |
+| `item8` | `int` |
+| `quantity8` | `int` |
+| `item9` | `int` |
+| `quantity9` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_campaigngoodiepacks"]`
+- `name [unique, name: "IX_campaigngoodiepacks"]`
+
+### Relations
+
+- Referenced by `campaigns.id`
+
+---
+
+## categoryFlags
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `[value]` | `bigint [not null]` |
+| `name` | `varchar(50)` |
+| `note` | `nvarchar(2048)` |
+| `hidden` | `bit [not null, default: 0]` |
+| `isunique` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `"[value]" [unique, name: "IX_categoryFlags"]`
+
+### Relations
+
+- `[value]` → `productiondecalibration.categoryflag`
+- `[value]` → `productionduration.category`
+
+---
+
+## categorygroups
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `groupId` | `int [not null]` |
+| `category` | `bigint [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## categorygroupsnames
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(100) [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## centralbanklog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventday` | `datetime [not null]` |
+| `amount` | `bigint [not null]` |
+
+---
+
+## centralbanktransactions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null]` |
+| `transactiontype` | `int [not null]` |
+| `amount` | `float [not null]` |
+| `bankcredit` | `bigint [not null]` |
+
+---
+
+## channelbans
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `memberid` | `int [not null]` |
+| `channelid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_channelbans"]`
+
+---
+
+## channelmembers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `channelid` | `int [not null]` |
+| `memberid` | `int [not null]` |
+| `role` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_channelmembers_"]`
+
+---
+
+## channels
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `nvarchar(50) [not null]` |
+| `password` | `nvarchar(50)` |
+| `topic` | `nvarchar(200)` |
+| `type` | `int [not null, default: 0]` |
+| `isForcedJoin` | `bit` |
+| `DiscordId` | `varchar(128)` |
+
+### Indexes
+
+- `id [pk, name: "PK_channels_"]`
+- `name [unique, name: "IX_channels_name"]`
+
+---
+
+## characterextensions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterextensionid` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `extensionlevel` | `int` |
+
+### Indexes
+
+- `characterextensionid [pk, name: "PK_characterextensions"]`
+
+### Relations
+
+- Referenced by `extensions.extensionid`
+
+---
+
+## characterhighscore
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `npcskilled` | `int [not null]` |
+| `playerskilled` | `int [not null]` |
+| `date` | `datetime` |
+
+### Indexes
+
+- `id [pk, name: "PK_characterhighscore"]`
+
+---
+
+## characterkillreports
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `reportid` | `uniqueidentifier [not null]` |
+| `victim` | `bit` |
+| `attacker` | `bit` |
+| `killer` | `bit` |
+
+### Indexes
+
+- `id [pk, name: "PK_characterkillreports"]`
+
+---
+
+## charactermessages
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `mailid` | `"bigint IDENTITY(1,1)" [not null]` |
+| `owner` | `int [not null]` |
+| `sender` | `int [not null]` |
+| `folder` | `int [not null]` |
+| `type` | `int [not null, default: 0]` |
+| `targets` | `varchar(512) [not null]` |
+| `creation` | `datetime [default: `getdate()`]` |
+| `subject` | `nvarchar(128)` |
+| `body` | `nvarchar(2000)` |
+| `wasread` | `bit [not null, default: 0]` |
+
+---
+
+## characternickhistory
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `accountid` | `int [not null]` |
+| `nick` | `varchar(50)` |
+| `eventdate` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## characternotes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `targetid` | `int [not null]` |
+| `note` | `nvarchar(2000) [not null]` |
+
+---
+
+## characternpcdeath
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null, default: 0]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `npcdefinition` | `int [not null]` |
+| `playersrobot` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+
+---
+
+## characterreimburselog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `containereid` | `bigint` |
+| `characterid` | `int` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `note` | `nvarchar(2048)` |
+
+---
+
+## characters
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterID` | `"int IDENTITY(1,1)" [not null]` |
+| `accountID` | `int` |
+| `rootEID` | `bigint [not null]` |
+| `nick` | `varchar(50)` |
+| `moodMessage` | `nvarchar(2000)` |
+| `creation` | `smalldatetime [default: `getdate()`]` |
+| `lastLogOut` | `smalldatetime` |
+| `lastUsed` | `smalldatetime` |
+| `credit` | `float [not null, default: 0]` |
+| `inUse` | `bit [not null, default: 0]` |
+| `totalMinsOnline` | `int [not null, default: 0]` |
+| `activeChassis` | `bigint` |
+| `active` | `bit [not null, default: 1]` |
+| `deletedAt` | `smalldatetime` |
+| `baseEID` | `bigint` |
+| `defaultcorporationEID` | `bigint` |
+| `majorID` | `int [not null, default: 0]` |
+| `raceID` | `int [not null, default: 0]` |
+| `schoolID` | `int [not null, default: 0]` |
+| `sparkID` | `int [not null, default: 0]` |
+| `lastdocked` | `datetime` |
+| `docked` | `bit [not null, default: 1]` |
+| `lastteleported` | `datetime` |
+| `zoneID` | `int` |
+| `nickcorrected` | `bit [not null, default: 0]` |
+| `offensivenick` | `bit [not null, default: 0]` |
+| `positionX` | `float` |
+| `positionY` | `float` |
+| `homeBaseEID` | `bigint` |
+| `blockTrades` | `bit [not null, default: 0]` |
+| `globalMute` | `bit [not null, default: 0]` |
+| `avatar` | `varchar(MAX)` |
+| `note` | `varchar(MAX)` |
+| `corporationeid` | `bigint [not null, default: 0]` |
+| `allianceeid` | `bigint` |
+| `language` | `int [not null, default: 0]` |
+| `LastRespec` | `datetime` |
+
+### Indexes
+
+- `characterID [pk, name: "PK_characters"]`
+- `nick [unique, name: "IX_nickUnique"]`
+
+---
+
+## charactersettings
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `settingsstring` | `nvarchar(MAX)` |
+
+---
+
+## charactersocial
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `friendid` | `int [not null]` |
+| `socialstate` | `tinyint [not null]` |
+| `note` | `nvarchar(2000)` |
+| `laststateupdate` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## charactersparks
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `sparkid` | `int [not null]` |
+| `active` | `bit [not null, default: 0]` |
+| `activationtime` | `datetime` |
+
+---
+
+## charactersparkteleports
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `baseeid` | `bigint [not null]` |
+| `basedefinition` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_charactersparkteleports"]`
+- `(characterid, baseeid) [unique, name: "IX_charactersparkteleports"]`
+
+---
+
+## charactertransactions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `transactiontype` | `int [not null]` |
+| `amount` | `float [not null]` |
+| `transactiondate` | `datetime [not null, default: `getdate()`]` |
+| `definition` | `int` |
+| `quantity` | `int` |
+| `currentcredit` | `float [not null, default: 0]` |
+| `othercharacter` | `int` |
+| `containereid` | `bigint` |
+
+---
+
+## chassisbonus
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null, default: 0]` |
+| `extension` | `int [not null]` |
+| `bonus` | `float [not null]` |
+| `note` | `nvarchar(2000)` |
+| `targetpropertyID` | `int [not null]` |
+| `effectenhancer` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_chassisbonus"]`
+- `(definition, extension, targetpropertyID) [unique, name: "IX_chassis_bonus"]`
+
+### Relations
+
+- Referenced by `aggregatefields.id`
+- Referenced by `entitydefaults.definition`
+- Referenced by `extensions.extensionid`
+
+---
+
+## cmails
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `owner` | `int [not null]` |
+| `sender` | `int [not null]` |
+| `target` | `int [not null]` |
+| `subject` | `nvarchar(128) [not null]` |
+| `body` | `nvarchar(2000) [not null]` |
+| `type` | `tinyint [not null]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `wasread` | `bit [not null, default: 0]` |
+| `folder` | `tinyint [not null]` |
+| `mailid` | `uniqueidentifier` |
+| `sourceid` | `uniqueidentifier` |
+
+---
+
+## combatlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `date` | `datetime [not null]` |
+| `zoneId` | `int [not null]` |
+| `characterId` | `int [not null]` |
+| `data` | `varchar(MAX) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_combatlog"]`
+
+---
+
+## components
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `componentdefinition` | `int [not null]` |
+| `componentamount` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_components"]`
+- `(definition, componentdefinition) [unique, name: "IX_components"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## connectedips
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `ipaddress` | `varchar(16) [not null]` |
+| `sessionstart` | `smalldatetime [not null, default: `getdate()`]` |
+| `banned` | `bit [not null, default: 0]` |
+| `note` | `nvarchar(512)` |
+| `bantime` | `smalldatetime` |
+| `bannedby` | `int` |
+| `clientid` | `int [not null, default: 0]` |
+| `accountid` | `int` |
+
+---
+
+## containerlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `containerEID` | `bigint [not null]` |
+| `memberID` | `int [not null]` |
+| `containeraccess` | `int [not null]` |
+| `operationdate` | `datetime [not null, default: `getdate()`]` |
+| `definition` | `int` |
+| `quantity` | `int` |
+
+---
+
+## corporationApplication
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterID` | `int [not null]` |
+| `corporationEID` | `bigint [not null]` |
+| `applyTime` | `smalldatetime [not null, default: `getdate()`]` |
+| `motivation` | `nvarchar(512)` |
+
+### Relations
+
+- Referenced by `corporations.eid`
+
+---
+
+## corporationceotakeover
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporationeid` | `bigint [not null]` |
+| `characterid` | `int [not null]` |
+| `expiry` | `datetime [not null]` |
+
+---
+
+## corporationdocumentconfig
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `documenttype` | `int [not null]` |
+| `creationprice` | `int [not null, default: 0]` |
+| `rentprice` | `int [not null, default: 0]` |
+| `rentperioddays` | `int [not null, default: 0]` |
+| `maxpercharacter` | `int [not null, default: 0]` |
+| `note` | `varchar(2048)` |
+
+### Indexes
+
+- `id [pk, name: "PK_corporationdocumentconfig"]`
+
+---
+
+## corporationdocumentregistration
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `documentid` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `role` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_corporationdocumentregistration"]`
+
+---
+
+## corporationdocuments
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `lastmodified` | `datetime [not null, default: `getdate()`]` |
+| `validuntil` | `datetime` |
+| `ownercharacterid` | `int [not null]` |
+| `documenttype` | `int [not null]` |
+| `version` | `int [not null, default: 0]` |
+| `body` | `nvarchar(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_corporationdocuments"]`
+
+---
+
+## corporationhistory
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterID` | `int [not null]` |
+| `corporationEID` | `bigint [not null]` |
+| `corporationJoined` | `smalldatetime [not null, default: `getdate()`]` |
+| `corporationLeft` | `smalldatetime` |
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_corporationhistory"]`
+
+### Relations
+
+- Referenced by `corporations.eid`
+
+---
+
+## corporationleave
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `leavetime` | `datetime [not null, default: `getdate()`]` |
+
+### Indexes
+
+- `characterid [unique, name: "IX_corporationleave"]`
+
+---
+
+## corporationlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `timestamp` | `datetime [not null, default: `getdate()`]` |
+| `corporationEid` | `bigint [not null]` |
+| `type` | `int [not null]` |
+| `issuerId` | `int [not null]` |
+| `memberId` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_corporationlog"]`
+
+---
+
+## corporationmembers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `corporationEID` | `bigint [not null]` |
+| `memberid` | `int [not null]` |
+| `role` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_corporationmembers"]`
+
+### Relations
+
+- Referenced by `corporations.eid`
+
+---
+
+## corporationnamehistory
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporationeid` | `bigint [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `nick` | `varchar(6) [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `characterid` | `int` |
+
+---
+
+## corporationrolehistory
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporationEID` | `bigint [not null]` |
+| `issuerID` | `int [not null]` |
+| `memberID` | `int [not null]` |
+| `oldrole` | `int [not null, default: 0]` |
+| `newrole` | `int [not null, default: 0]` |
+| `rolesettime` | `datetime [not null, default: `getdate()`]` |
+
+### Relations
+
+- Referenced by `corporations.eid`
+
+---
+
+## corporationtransactions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporationEID` | `bigint [not null]` |
+| `memberID` | `int` |
+| `transactiontype` | `int [not null]` |
+| `amount` | `float [not null]` |
+| `transactiondate` | `datetime [not null, default: `getdate()`]` |
+| `quantity` | `int` |
+| `definition` | `int` |
+| `targetMemberID` | `int` |
+| `currentwallet` | `float [not null, default: 0]` |
+| `involvedCorporationEID` | `bigint` |
+
+---
+
+## countries
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `int [not null]` |
+| `country` | `varchar(50) [not null]` |
+| `nick` | `varchar(8)` |
+
+---
+
+## cw_race
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `raceid` | `int [not null]` |
+| `name` | `nvarchar(50) [not null]` |
+| `attributeA` | `float [not null]` |
+| `attributeB` | `float [not null]` |
+| `attributeC` | `float [not null]` |
+| `attributeD` | `float [not null]` |
+| `attributeE` | `float [not null]` |
+| `attributeF` | `float [not null]` |
+| `note` | `nvarchar(2048)` |
+| `descriptiontoken` | `varchar(50)` |
+
+### Relations
+
+- `raceid` → `cw_school.raceid`
+- `raceid` → `cw_race_extension.raceid`
+
+---
+
+## cw_school
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `schoolid` | `int [not null]` |
+| `raceid` | `int` |
+| `name` | `nvarchar(50) [not null]` |
+| `attributeA` | `float [not null]` |
+| `attributeB` | `float [not null]` |
+| `attributeC` | `float [not null]` |
+| `attributeD` | `float [not null]` |
+| `attributeE` | `float [not null]` |
+| `attributeF` | `float [not null]` |
+| `note` | `nvarchar(2048)` |
+| `descriptiontoken` | `varchar(50)` |
+
+### Relations
+
+- Referenced by `cw_race.raceid`
+- `schoolid` → `cw_corporation.schoolid`
+- `schoolid` → `cw_major.schoolid`
+- `schoolid` → `cw_school_extension.schoolid`
+
+---
+
+## cw_corporation
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `corporationEID` | `bigint` |
+| `schoolid` | `int` |
+| `name` | `nvarchar(50) [not null]` |
+| `attributeA` | `float [not null]` |
+| `attributeB` | `float [not null]` |
+| `attributeC` | `float [not null]` |
+| `attributeD` | `float [not null]` |
+| `attributeE` | `float [not null]` |
+| `attributeF` | `float [not null]` |
+| `note` | `nvarchar(2048)` |
+| `descriptiontoken` | `varchar(50)` |
+| `baseEID` | `bigint` |
+| `missionstatement` | `varchar(50)` |
+
+### Indexes
+
+- `id [pk, name: "PK_cw_corporation_ix"]`
+- `corporationEID [unique, name: "IX_cw_corporation"]`
+
+### Relations
+
+- Referenced by `corporations.eid`
+- Referenced by `cw_school.schoolid`
+- `corporationEID` → `cw_corporation_extension.corporationEID`
+
+---
+
+## cw_corporation_extension
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporation_extension_id` | `"int IDENTITY(1,1)" [not null]` |
+| `corporationEID` | `bigint [not null]` |
+| `extensionid` | `int [not null]` |
+| `levelincrement` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `corporation_extension_id [pk, name: "PK_cw_corporation_extension"]`
+- `(corporationEID, extensionid) [unique, name: "IX_cw_corporation_extension"]`
+
+### Relations
+
+- Referenced by `cw_corporation.corporationEID`
+- Referenced by `extensions.extensionid`
+
+---
+
+## cw_major
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `majorid` | `int [not null]` |
+| `schoolid` | `int` |
+| `name` | `nvarchar(50) [not null]` |
+| `attributeA` | `float [not null]` |
+| `attributeB` | `float [not null]` |
+| `attributeC` | `float [not null]` |
+| `attributeD` | `float [not null]` |
+| `attributeE` | `float [not null]` |
+| `attributeF` | `float [not null]` |
+| `note` | `nvarchar(2048)` |
+| `descriptiontoken` | `varchar(50)` |
+
+### Relations
+
+- Referenced by `cw_school.schoolid`
+- `majorid` → `cw_major_extension.majorid`
+
+---
+
+## cw_major_extension
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `major_extension_id` | `"int IDENTITY(1,1)" [not null]` |
+| `majorid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `levelincrement` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `(majorid, extensionid) [unique, name: "IX_cw_major_extension"]`
+
+### Relations
+
+- Referenced by `cw_major.majorid`
+- Referenced by `extensions.extensionid`
+
+---
+
+## cw_race_extension
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `race_extension_id` | `"int IDENTITY(1,1)" [not null]` |
+| `raceid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `levelincrement` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `(raceid, extensionid) [unique, name: "IX_cw_race_extension"]`
+
+### Relations
+
+- Referenced by `cw_race.raceid`
+- Referenced by `extensions.extensionid`
+
+---
+
+## cw_school_extension
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `school_extension_id` | `"int IDENTITY(1,1)" [not null]` |
+| `schoolid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `levelincrement` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `(schoolid, extensionid) [unique, name: "IX_cw_school_extension"]`
+
+### Relations
+
+- Referenced by `cw_school.schoolid`
+- Referenced by `extensions.extensionid`
+
+---
+
+## cw_spark
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `sparkid` | `int [not null]` |
+| `name` | `nvarchar(50) [not null]` |
+| `attributeA` | `float [not null]` |
+| `attributeB` | `float [not null]` |
+| `attributeC` | `float [not null]` |
+| `attributeD` | `float [not null]` |
+| `attributeE` | `float [not null]` |
+| `attributeF` | `float [not null]` |
+| `note` | `nvarchar(2048)` |
+| `descriptiontoken` | `varchar(50)` |
+
+### Relations
+
+- `sparkid` → `cw_spark_extension.sparkid`
+
+---
+
+## cw_spark_extension
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `spark_extension_id` | `"int IDENTITY(1,1)" [not null]` |
+| `sparkid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `levelincrement` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `(sparkid, extensionid) [unique, name: "IX_cw_spark_extension"]`
+
+### Relations
+
+- Referenced by `cw_spark.sparkid`
+- Referenced by `extensions.extensionid`
+
+---
+
+## decorcategories
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `categoryname` | `varchar(256) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_decorcategories"]`
+- `categoryname [unique, name: "IX_decorcategories"]`
+
+### Relations
+
+- `id` → `decor.category`
+
+---
+
+## decor
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `quaternionx` | `float [not null]` |
+| `quaterniony` | `float [not null]` |
+| `quaternionz` | `float [not null]` |
+| `quaternionw` | `float [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+| `z` | `int [not null]` |
+| `scale` | `float [not null, default: 1]` |
+| `changed` | `bit [not null, default: 1]` |
+| `fadedistance` | `float [not null, default: 0]` |
+| `category` | `int [not null, default: 1]` |
+| `locked` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_decor"]`
+
+### Relations
+
+- Referenced by `decorcategories.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## defaultfieldscalculation
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `fieldname` | `nvarchar(50) [not null]` |
+| `formula` | `int [not null]` |
+| `display` | `bit [not null]` |
+| `runtime` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_defaultfieldscalculation"]`
+
+---
+
+## definitionconfig
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `targetdefinition` | `int` |
+| `summonerscount` | `int` |
+| `npcpresenceid` | `int` |
+| `item_work_range` | `float` |
+| `explosion_radius` | `float` |
+| `cycle_time` | `int` |
+| `damage_chemical` | `float` |
+| `damage_explosive` | `float` |
+| `damage_kinetic` | `float` |
+| `damage_thermal` | `float` |
+| `lifetime` | `int` |
+| `activationtime` | `int` |
+| `waves` | `int` |
+| `missionrelated` | `bit` |
+| `constructionradius` | `int` |
+| `action_delay` | `int` |
+| `deploy_radius` | `int` |
+| `transmitradius` | `int` |
+| `constructionlevelmax` | `int` |
+| `blockingradius` | `int` |
+| `chargeamount` | `int` |
+| `inconnections` | `int` |
+| `outconnections` | `int` |
+| `coretransferred` | `float` |
+| `transferefficiency` | `float` |
+| `productionupgradeamount` | `int` |
+| `productionlevel` | `int` |
+| `coreconsumption` | `float` |
+| `effectid` | `int` |
+| `corecalories` | `float` |
+| `corekickstartthreshold` | `float` |
+| `reinforcecountermax` | `int` |
+| `bandwidthusage` | `int` |
+| `bandwidthcapacity` | `int` |
+| `emitradius` | `int` |
+| `tint` | `varchar(50)` |
+| `typeexclusiverange` | `int` |
+| `network_node_range` | `int` |
+| `hitsize` | `float` |
+| `note` | `varchar(2000)` |
+| `damage_toxic` | `float` |
+
+### Indexes
+
+- `id [pk, name: "PK_deployablerelation"]`
+- `definition [unique, name: "IX_definitionconfig"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## definitionconfigunits
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `configname` | `varchar(128) [not null]` |
+| `measurementoffset` | `float [not null, default: 0]` |
+| `measurementmultiplier` | `float [not null, default: 1]` |
+| `digits` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_definitionconfigunits"]`
+- `configname [unique, name: "IX_definitionconfigunits"]`
+
+---
+
+## dynamiccalibrationtemplates
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `materialefficiency` | `float [not null, default: 0.5]` |
+| `timeefficiency` | `float [not null, default: 0.5]` |
+| `targetdefinition` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_dynamiccalibrationtemplates"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## effectcategories
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `name` | `nvarchar(50) [not null]` |
+| `flag` | `bigint [not null]` |
+| `maxlevel` | `int [not null, default: 0]` |
+| `note` | `nvarchar(2048)` |
+
+### Indexes
+
+- `flag [pk, name: "PK_effectcategories"]`
+- `name [unique, name: "IX_effectcategories"]`
+
+---
+
+## effectdefaultmodifiers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `effectid` | `int [not null]` |
+| `field` | `int [not null]` |
+| `[value]` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_effectdefaultmodifiers"]`
+
+### Relations
+
+- Referenced by `aggregatefields.id`
+
+---
+
+## effects
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `effectcategory` | `bigint [not null, default: 0]` |
+| `duration` | `int [not null, default: 0]` |
+| `name` | `nvarchar(50) [not null]` |
+| `description` | `nvarchar(2048) [not null]` |
+| `note` | `nvarchar(2048)` |
+| `isaura` | `bit [not null, default: 0]` |
+| `auraradius` | `int [not null, default: 0]` |
+| `ispositive` | `bit [not null, default: 0]` |
+| `display` | `int [not null, default: 0]` |
+| `saveable` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_effects"]`
+- `name [unique, name: "IX_effects"]`
+
+---
+
+## enablerextensions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `extensionlevel` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_enablerextensions"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `extensions.extensionid`
+
+---
+
+## entities
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `definition` | `int [not null]` |
+| `owner` | `bigint` |
+| `parent` | `bigint` |
+| `health` | `float [not null, default: 100]` |
+| `ename` | `nvarchar(128)` |
+| `quantity` | `int [not null, default: 1]` |
+| `repackaged` | `bit [not null, default: 0]` |
+| `dynprop` | `varchar(MAX)` |
+
+### Indexes
+
+- `eid [pk, name: "PK_newentities"]`
+
+---
+
+## entitystorage
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `storage_name` | `nvarchar(50) [not null]` |
+| `eid` | `bigint` |
+
+---
+
+## entitytemplates
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `parent` | `int [not null, default: 0]` |
+| `name` | `varchar(50)` |
+
+### Indexes
+
+- `id [pk, name: "PK_entitytemplates"]`
+
+---
+
+## entitytrash
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `deleted` | `datetime [not null, default: `getdate()`]` |
+| `wasinsured` | `bit [not null, default: 0]` |
+| `killedbyplayer` | `bit [not null, default: 0]` |
+| `inactiveperiod` | `int [not null, default: 0]` |
+| `dctime` | `datetime` |
+
+---
+
+## environmentdescription
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `descriptionstring` | `varchar(MAX) [not null]` |
+
+### Indexes
+
+- `definition [unique, name: "IX_environmentdescription"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## environmentdescriptionstaging
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `descriptionstring` | `varchar(MAX) [not null]` |
+
+### Indexes
+
+- `definition [unique, name: "IX_environmentdescriptionstaging"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## epforactivitylog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `accountid` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `epforactivitytype` | `int [not null]` |
+| `rawpoints` | `int [not null]` |
+| `points` | `int [not null]` |
+| `boostfactor` | `float [not null]` |
+| `multiplier` | `int` |
+| `bonusMultiplier` | `int [default: 0]` |
+
+---
+
+## extensionpointpenalty
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `points` | `int [not null]` |
+| `penaltytype` | `int [not null]` |
+| `forever` | `bit [not null, default: 0]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## extensionpoints
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `points` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## extensionpointworklog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `total` | `int [not null, default: 0]` |
+| `paying` | `int [not null]` |
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+
+---
+
+## extensionprerequire
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `extensionprerequireid` | `"int IDENTITY(1,1)" [not null]` |
+| `extensionid` | `int [not null]` |
+| `requiredextension` | `int [not null]` |
+| `requiredlevel` | `int [not null]` |
+
+### Relations
+
+- Referenced by `extensions.extensionid`
+- Referenced by `extensions.extensionid`
+
+---
+
+## extensionremovelog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `extensionlevel` | `int [not null]` |
+| `points` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+
+---
+
+## extensionsubscription
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int [not null]` |
+| `starttime` | `datetime [not null, default: `getdate()`]` |
+| `endtime` | `datetime [not null]` |
+| `multiplierBonus` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_extensionsubscription"]`
+
+---
+
+## facilitymap
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `defname` | `varchar(50) [not null]` |
+| `leveltag` | `nvarchar(50)` |
+
+### Indexes
+
+- `id [pk, name: "PK_facilitymap"]`
+
+---
+
+## gameglobals
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `clockoffset` | `bigint [not null]` |
+| `active` | `smalldatetime` |
+| `bankcredit` | `bigint [not null, default: 0]` |
+| `lastonline` | `datetime` |
+
+---
+
+## gang
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `uniqueidentifier [not null]` |
+| `leaderid` | `int [not null, default: 0]` |
+| `name` | `nvarchar(50)` |
+
+### Indexes
+
+- `id [pk, name: "PK_gang"]`
+
+### Relations
+
+- `id` → `gangmembers.gangid`
+
+---
+
+## gangmembers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `gangid` | `uniqueidentifier [not null]` |
+| `memberid` | `int [not null]` |
+| `role` | `int [not null, default: 0]` |
+
+### Relations
+
+- Referenced by `gang.id`
+
+---
+
+## giftloots
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `minquantity` | `int [not null, default: 1]` |
+| `maxquantity` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_giftloots"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## hardwareinfo
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountid` | `int [not null]` |
+| `gfxcard` | `varchar(128) [not null]` |
+| `gfxdriver` | `varchar(128)` |
+| `gfxvendorid` | `int [not null]` |
+| `gfxdeviceid` | `int [not null]` |
+| `gfxdriverversion` | `bigint [not null]` |
+| `pixelshader` | `bigint [not null]` |
+| `vertexshader` | `bigint [not null]` |
+| `maxtexturex` | `int [not null]` |
+| `maxtexturey` | `int [not null]` |
+| `osversion` | `varchar(128)` |
+
+### Indexes
+
+- `accountid [pk, name: "PK_hardwareinfo"]`
+
+---
+
+## harvestlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `smalldatetime [not null]` |
+| `zoneid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `amount` | `int [not null]` |
+
+---
+
+## hostconfig
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `hostname` | `varchar(50) [not null]` |
+| `hostip` | `varchar(50) [not null, default: 'host ip goes here']` |
+| `hostport` | `int [not null, default: 18000]` |
+| `sequenceid` | `int [not null, default: 0]` |
+| `monitor` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `sequenceid [unique, name: "IX_hostconfig"]`
+
+---
+
+## icetracker
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `usedbytrial` | `bit [not null, default: 0]` |
+| `usedat` | `datetime [not null, default: `getdate()`]` |
+| `characterid` | `int [not null]` |
+
+---
+
+## insurance
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `characterid` | `int [not null]` |
+| `corporationeid` | `bigint` |
+| `insurancetype` | `int [not null]` |
+| `enddate` | `datetime [not null]` |
+| `payout` | `float [not null]` |
+
+---
+
+## insuranceprices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `fee` | `float [not null, default: 0]` |
+| `payout` | `float [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_insuranceprices"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## intrusiondockingrightslog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int` |
+| `siteeid` | `bigint [not null]` |
+| `dockingstandinglimit` | `float` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `owner` | `bigint` |
+| `eventtype` | `int [not null, default: 0]` |
+
+---
+
+## intrusioneffectlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int` |
+| `siteeid` | `bigint [not null]` |
+| `effectid` | `int` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `owner` | `bigint` |
+| `eventtype` | `int [not null, default: 0]` |
+
+---
+
+## intrusionloot
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `sitedefinition` | `int [not null]` |
+| `sapdefinition` | `int [not null]` |
+| `itemdefinition` | `int [not null]` |
+| `minquantity` | `int [not null, default: 1]` |
+| `maxquantity` | `int [not null]` |
+| `minstabilitythreshold` | `int [not null]` |
+| `maxstabilitythreshold` | `int [not null]` |
+| `probability` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_intrusionloot"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## intrusionproductionlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `siteeid` | `bigint [not null]` |
+| `eventtype` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `facilitydefinition` | `int` |
+| `facilitylevel` | `int` |
+| `oldfacilitylevel` | `int` |
+| `characterid` | `int` |
+| `points` | `int` |
+| `oldpoints` | `int` |
+| `owner` | `bigint` |
+
+---
+
+## intrusionproductionstack
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `siteeid` | `bigint [not null]` |
+| `facilityeid` | `bigint [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+
+### Indexes
+
+- `id [pk, name: "PK_intrusionproductionstack"]`
+
+---
+
+## intrusionsapdeploylog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `deploytime` | `datetime [not null, default: `getdate()`]` |
+| `siteeid` | `bigint [not null]` |
+| `sapdefinition` | `int [not null]` |
+
+---
+
+## intrusionsaps
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `siteeid` | `bigint [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `name` | `varchar(128) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_intrusionsaps"]`
+
+---
+
+## intrusionsitelog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `siteeid` | `bigint [not null]` |
+| `owner` | `bigint` |
+| `stability` | `int [not null]` |
+| `winnercorporationeid` | `bigint` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `sapdefinition` | `int [not null]` |
+| `oldstability` | `int [not null, default: 0]` |
+| `oldowner` | `bigint` |
+| `eventtype` | `int [not null, default: 0]` |
+
+---
+
+## intrusionsitemessagelog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `siteeid` | `bigint [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `characterid` | `int [not null]` |
+| `message` | `nvarchar(256)` |
+| `owner` | `bigint` |
+| `eventtype` | `int [not null, default: 0]` |
+
+---
+
+## intrusionsites
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `siteeid` | `bigint [not null]` |
+| `owner` | `bigint` |
+| `enabled` | `bit [not null, default: 1]` |
+| `stability` | `int [not null, default: 0]` |
+| `dockingstandinglimit` | `float` |
+| `dockingcontroltime` | `datetime` |
+| `seteffectcontroltime` | `datetime` |
+| `activeeffectid` | `int` |
+| `message` | `nvarchar(256)` |
+| `productionpoints` | `int [not null, default: 0]` |
+| `intrusionstarttime` | `datetime` |
+| `defensestandinglimit` | `float` |
+| `note` | `varchar(128)` |
+| `isAnnounced` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_intrusionsites"]`
+
+---
+
+## intrusionsitestabilitythreshold
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `categoryflag` | `bigint [not null]` |
+| `threshold` | `int [not null]` |
+| `bonustype` | `int [not null]` |
+| `effecttype` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_intrusionsitethreshold"]`
+
+---
+
+## itemcreation
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `Id` | `"int IDENTITY(1,1)" [not null]` |
+| `Type` | `nvarchar(50) [not null]` |
+| `Entity` | `int [not null]` |
+| `Qty` | `int [not null]` |
+| `CharacterId` | `int [not null]` |
+| `IsTraining` | `int [not null]` |
+| `ZoneId` | `int [not null]` |
+| `DateTime` | `datetime [not null]` |
+
+### Indexes
+
+- `Id [pk]`
+
+---
+
+## itemprices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `price` | `float [not null]` |
+| `profitrate` | `float [not null, default: 1]` |
+| `manualprice` | `bit [not null, default: 0]` |
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## itemresearchlevels
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `researchlevel` | `int [not null, default: 1]` |
+| `calibrationprogram` | `int` |
+| `enabled` | `bit [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_itemresearchlevels"]`
+- `definition [unique, name: "IX_itemresearchlevels"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## itemscore
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `score` | `int [not null]` |
+
+---
+
+## itemshop
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `presetid` | `int [not null]` |
+| `targetdefinition` | `int [not null]` |
+| `targetamount` | `int [not null, default: 1]` |
+| `tmcoin` | `int [default: 1]` |
+| `icscoin` | `int` |
+| `asicoin` | `int` |
+| `credit` | `float` |
+| `unicoin` | `int [default: 1]` |
+| `globallimit` | `int` |
+| `purchasecount` | `int [not null, default: 0]` |
+| `standing` | `float` |
+
+### Indexes
+
+- `id [pk, name: "PK_itemshop"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## itemshoppresets
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `note` | `nvarchar(2000)` |
+
+### Indexes
+
+- `id [pk, name: "PK_itemshoppresets"]`
+
+### Relations
+
+- `id` → `itemshoplocations.presetid`
+
+---
+
+## itemshoplocations
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `locationeid` | `bigint [not null]` |
+| `presetid` | `int [not null]` |
+| `note` | `nvarchar(2000)` |
+
+### Indexes
+
+- `locationeid [pk, name: "PK_itemshoplocations"]`
+- `(locationeid, presetid) [unique, name: "IX_itemshoplocations"]`
+
+### Relations
+
+- Referenced by `itemshoppresets.id`
+
+---
+
+## killreports
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `uniqueidentifier [not null]` |
+| `date` | `datetime [not null]` |
+| `data` | `varchar(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_killreports"]`
+
+---
+
+## locktest
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `eid` | `bigint [not null]` |
+
+---
+
+## lootitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `uniqueidentifier` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `health` | `float [not null]` |
+| `repackaged` | `bit [not null]` |
+| `containereid` | `bigint [not null]` |
+
+---
+
+## lotteryitemweights
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `lotterydefinition` | `int [not null]` |
+| `categoryflags` | `bigint [not null]` |
+| `tiertype` | `int [not null]` |
+| `tierlevel` | `int [not null]` |
+| `weight` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_lotteryitemweights"]`
+
+---
+
+## market_orders_configuration
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definitionname` | `varchar(100) [not null]` |
+| `amount` | `int [not null]` |
+
+---
+
+## marketaverageprices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `marketeid` | `bigint [not null]` |
+| `itemdefinition` | `int [not null]` |
+| `totalprice` | `float [not null]` |
+| `quantity` | `bigint [not null]` |
+| `date` | `smalldatetime [not null]` |
+| `dailylowest` | `float [not null, default: 0]` |
+| `dailyhighest` | `float [not null, default: 0]` |
+
+---
+
+## marketaveragesbycomponent
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `definition` | `int [not null]` |
+| `price` | `float [not null]` |
+
+---
+
+## marketitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `marketitemid` | `"int IDENTITY(1,1)" [not null]` |
+| `marketeid` | `bigint [not null]` |
+| `itemeid` | `bigint` |
+| `itemdefinition` | `int [not null]` |
+| `submittereid` | `bigint [not null]` |
+| `submitted` | `smalldatetime [not null, default: `getdate()`]` |
+| `duration` | `int [not null, default: 0]` |
+| `isSell` | `bit [not null]` |
+| `price` | `float [not null]` |
+| `quantity` | `int [not null, default: 1]` |
+| `usecorporationwallet` | `bit [not null, default: 0]` |
+| `isvendoritem` | `bit [not null, default: 0]` |
+| `formembersof` | `bigint` |
+| `isAutoOrder` | `bit` |
+
+### Indexes
+
+- `marketitemid [pk, name: "PK_marketitems"]`
+
+---
+
+## markettaxlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `owner` | `bigint [not null]` |
+| `characterid` | `int [not null]` |
+| `baseeid` | `bigint [not null]` |
+| `changefrom` | `float [not null]` |
+| `changeto` | `float [not null]` |
+
+---
+
+## mineralconfigs
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `materialtype` | `int [not null]` |
+| `maxnodes` | `int [not null]` |
+| `maxtilespernode` | `int [not null, default: 0]` |
+| `totalamountpernode` | `int [not null, default: 0]` |
+| `minthreshold` | `float [not null, default: 0.0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_mineralconfigs"]`
+- `(zoneid, materialtype) [unique, name: "IX_mineralconfigs"]`
+
+---
+
+## mineralnodes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `materialtype` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+| `width` | `int [not null]` |
+| `height` | `int [not null]` |
+| `data` | `varbinary(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_mineralnodes"]`
+
+---
+
+## minerals
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `idx` | `int [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `definition` | `int [not null]` |
+| `amount` | `int [not null]` |
+| `extractionType` | `int [not null]` |
+| `enablereffectrequired` | `bit [not null, default: 0]` |
+| `note` | `nvarchar(1024)` |
+| `geoscandocument` | `int` |
+
+---
+
+## mineralscan
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `ownerid` | `int [not null]` |
+| `materialprobetype` | `tinyint [not null, default: 0]` |
+| `creation` | `datetime [not null]` |
+| `zoneid` | `int [not null]` |
+| `materialtype` | `tinyint [not null]` |
+| `x1` | `int [not null]` |
+| `y1` | `int [not null]` |
+| `x2` | `int [not null]` |
+| `y2` | `int [not null]` |
+| `scanAccuracy` | `float [not null, default: 0.0]` |
+| `folder` | `nvarchar(32)` |
+| `quality` | `bigint [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_mineralscan"]`
+
+---
+
+## mininglog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `smalldatetime [not null]` |
+| `zoneid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `amount` | `int [not null]` |
+
+---
+
+## missionagents
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `agentname` | `varchar(128) [not null]` |
+| `owner` | `bigint` |
+| `ownername` | `varchar(128)` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionagents"]`
+
+### Relations
+
+- `id` → `missions.sourceagent`
+
+---
+
+## missionbonus
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `missioncategory` | `int [not null]` |
+| `missionlevel` | `int [not null]` |
+| `agentid` | `int [not null]` |
+| `bonus` | `int [not null]` |
+
+---
+
+## missionconstants
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `name` | `varchar(50) [not null]` |
+| `[value]` | `float [not null, default: 1]` |
+
+### Indexes
+
+- `name [unique, name: "IX_missionconstants"]`
+
+---
+
+## zones
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `int [not null]` |
+| `x` | `int [not null, default: 0]` |
+| `y` | `int [not null, default: 0]` |
+| `name` | `nvarchar(50) [not null]` |
+| `description` | `varchar(50)` |
+| `note` | `varchar(2048)` |
+| `fertility` | `int [not null, default: 60]` |
+| `zoneplugin` | `nvarchar(50)` |
+| `zoneip` | `varchar(50)` |
+| `zoneport` | `int [not null, default: 0]` |
+| `isinstance` | `bit [not null, default: 0]` |
+| `enabled` | `bit [not null, default: 0]` |
+| `spawnid` | `int` |
+| `plantruleset` | `int [not null, default: 0]` |
+| `protected` | `bit [not null, default: 0]` |
+| `raceid` | `int [not null, default: 1]` |
+| `width` | `int [not null, default: 2048]` |
+| `height` | `int [not null, default: 2048]` |
+| `terraformable` | `bit [not null, default: 0]` |
+| `zonetype` | `int [not null, default: 1]` |
+| `sparkcost` | `int [not null, default: 0]` |
+| `maxdockingbase` | `int [not null, default: 0]` |
+| `sleeping` | `bit [not null, default: 1]` |
+| `plantaltitudescale` | `float [not null, default: 1]` |
+| `host` | `varchar(50)` |
+| `active` | `bit [not null, default: 1]` |
+| `timeLimitMinutes` | `int` |
+| `pbsTechLimit` | `int` |
+| `PlantsGrowthTimerOverrideMin` | `int` |
+
+### Relations
+
+- `id` → `teleportdescriptions.sourcezone`
+- `id` → `teleportdescriptions.targetzone`
+- `id` → `zoneentities.zoneID`
+- `id` → `zonesectors.zoneid`
+
+---
+
+## teleportdescriptions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `description` | `varchar(128) [not null]` |
+| `sourcecolumn` | `bigint` |
+| `targetcolumn` | `bigint` |
+| `sourcezone` | `int` |
+| `sourcerange` | `int` |
+| `targetzone` | `int` |
+| `targetx` | `float` |
+| `targety` | `float` |
+| `targetz` | `float` |
+| `targetrange` | `int [default: 1]` |
+| `usetimeout` | `int [not null, default: 0]` |
+| `listable` | `bit [not null, default: 0]` |
+| `active` | `bit [not null, default: 1]` |
+| `type` | `int [not null, default: 0]` |
+| `sourcecolumnname` | `nvarchar(128)` |
+| `targetcolumnname` | `nvarchar(128)` |
+
+### Indexes
+
+- `id [pk, name: "PK_teleports"]`
+- `description [unique, name: "IX_teleportdescriptions"]`
+
+### Relations
+
+- Referenced by `zones.id`
+- Referenced by `zones.id`
+- `id` → `missionenterpoints.teleportchannelid`
+
+---
+
+## missiontypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `int [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `category` | `varchar(50)` |
+| `categoryvalue` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiontypes"]`
+
+### Relations
+
+- `id` → `missions.missiontype`
+
+---
+
+## missionissuer
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128)` |
+| `corporationeid` | `bigint` |
+| `corporationname` | `varchar(60)` |
+| `allianceeid` | `bigint` |
+| `alliancename` | `varchar(60)` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionissuercorporation"]`
+
+### Relations
+
+- `id` → `missions.issuerid`
+
+---
+
+## missions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128)` |
+| `title` | `varchar(128) [not null, default: 'title']` |
+| `description` | `varchar(128) [not null, default: 'description']` |
+| `missiontype` | `int [not null, default: 0]` |
+| `issuerid` | `int` |
+| `missionidonfail` | `int` |
+| `missionidonsuccess` | `int` |
+| `isunique` | `bit [not null, default: 0]` |
+| `note` | `nvarchar(2000)` |
+| `missionpack` | `int [not null, default: 0]` |
+| `periodminutes` | `int` |
+| `missionlevel` | `int` |
+| `durationminutes` | `int [not null, default: 360]` |
+| `successmessage` | `varchar(128)` |
+| `failmessage` | `varchar(128)` |
+| `listable` | `bit [not null, default: 1]` |
+| `alwaysenabled` | `bit [not null, default: 0]` |
+| `rewardfee` | `float [not null, default: 0]` |
+| `locationid` | `int` |
+| `behaviourtype` | `int [not null, default: 2]` |
+| `sourceagent` | `int` |
+| `difficultyreward` | `int` |
+| `difficultymultiplier` | `float` |
+
+### Indexes
+
+- `id [pk, name: "PK_missions"]`
+
+### Relations
+
+- Referenced by `missionagents.id`
+- Referenced by `missionissuer.id`
+- `id` → `missions.missionidonfail`
+- Referenced by `missions.id`
+- `id` → `missions.missionidonsuccess`
+- Referenced by `missions.id`
+- Referenced by `missiontypes.id`
+- `id` → `missionenterpoints.missionid`
+- `id` → `missionrequiredextensions.missionid`
+- `id` → `missionrequiredmissions.mission`
+- `id` → `missionrequiredmissions.requiredmission`
+- `id` → `missionrequiredstanding.missionid`
+- `id` → `missionrewards.missionid`
+- `id` → `missionstandingchange.missionid`
+- `id` → `missionstartitem.missionid`
+
+---
+
+## missionenterpoints
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `teleportchannelid` | `int [not null]` |
+| `missionid` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionenterpoints"]`
+
+### Relations
+
+- Referenced by `missions.id`
+- Referenced by `teleportdescriptions.id`
+
+---
+
+## missiongrind
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `missionlevel` | `int [not null, default: 0]` |
+| `amount` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiongrind"]`
+
+---
+
+## missionlocations
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `agentid` | `int [not null]` |
+| `locationeid` | `bigint [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `float [not null]` |
+| `y` | `float [not null]` |
+| `maxmissionlevel` | `int [not null, default: 6]` |
+| `locationname` | `nvarchar(128)` |
+| `dontsync` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionlocations"]`
+- `locationeid [unique, name: "IX_missionlocations_locationeid_unique"]`
+- `(agentid, locationeid) [unique, name: "IX_missionlocations_unique"]`
+
+---
+
+## missionlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `missionGuid` | `uniqueidentifier [not null]` |
+| `missionID` | `int [not null]` |
+| `characterID` | `int [not null]` |
+| `started` | `datetime [not null, default: `getdate()`]` |
+| `finished` | `datetime` |
+| `succeeded` | `bit [not null, default: 0]` |
+| `expire` | `datetime` |
+| `grouporder` | `int [not null, default: 0]` |
+| `spreadingang` | `bit [not null, default: 0]` |
+| `bonusmultiplier` | `float [not null, default: 0]` |
+| `locationid` | `int` |
+| `missionlevel` | `int` |
+| `issuercorporationeid` | `bigint` |
+| `issuerallianceeid` | `bigint` |
+| `selectedrace` | `int` |
+| `rewarddivider` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `missionGuid [unique, name: "IX_missionlog_guid"]`
+
+### Relations
+
+- `missionGuid` → `missiontargetsarchive.missionguid`
+
+---
+
+## missionparticipants
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"bigint IDENTITY(1,1)" [not null]` |
+| `missionguid` | `uniqueidentifier [not null]` |
+| `characterid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionparticipants"]`
+
+---
+
+## missionpayoutlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `missionguid` | `uniqueidentifier [not null]` |
+| `missionid` | `int [not null]` |
+| `missioncategory` | `int [not null]` |
+| `missionlevel` | `int [not null, default: 0]` |
+| `corporationeid` | `bigint` |
+| `characterid` | `int` |
+| `gangsize` | `int [not null, default: 0]` |
+| `amount` | `float [not null]` |
+| `sumamount` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionpayoutlog"]`
+
+---
+
+## missionrequiredextensions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `extensionid` | `int [not null]` |
+| `extensionlevel` | `int [not null]` |
+| `missionid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionrequiredextensions"]`
+
+### Relations
+
+- Referenced by `extensions.extensionid`
+- Referenced by `missions.id`
+
+---
+
+## missionrequiredmissions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `mission` | `int [not null]` |
+| `requiredmission` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionrequiredmissions"]`
+- `(mission, requiredmission) [unique, name: "IX_missionrequiredmissions_unique"]`
+
+### Relations
+
+- Referenced by `missions.id`
+- Referenced by `missions.id`
+
+---
+
+## missionrequiredstanding
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `missionid` | `int [not null]` |
+| `corporationeid` | `bigint` |
+| `standingabove` | `bit [not null]` |
+| `standingthreshold` | `float [not null]` |
+| `corporationname` | `varchar(128)` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionrequiredstanding"]`
+- `(corporationeid, missionid) [unique, name: "IX_missionrequiredstanding"]`
+
+### Relations
+
+- Referenced by `corporations.eid`
+- Referenced by `missions.id`
+
+---
+
+## missionrewards
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128)` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `probability` | `int [not null, default: 0]` |
+| `missionid` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionrewards2"]`
+
+### Relations
+
+- Referenced by `missions.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## missionspotinfo
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `type` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionspotinfo"]`
+
+---
+
+## missionstandingchange
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `missionid` | `int [not null, default: 0]` |
+| `change` | `float [not null, default: 0]` |
+| `alliancename` | `varchar(50)` |
+| `allianceeid` | `bigint` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionstandingchange"]`
+
+### Relations
+
+- Referenced by `missions.id`
+
+---
+
+## missionstartitem
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null, default: 1]` |
+| `missionid` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missionstartitem"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `missions.id`
+
+---
+
+## missiontargettypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `int [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `reward` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiontargettypes"]`
+
+### Relations
+
+- `id` → `missiontargets.targettype`
+
+---
+
+## missiontargets
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(512)` |
+| `description` | `varchar(128)` |
+| `missionid` | `int` |
+| `targettype` | `int [not null, default: 1]` |
+| `definition` | `int` |
+| `quantity` | `int` |
+| `targetpositionx` | `int` |
+| `targetpositiony` | `int` |
+| `targetpositionrange` | `int` |
+| `targetpositionzone` | `int` |
+| `scantype` | `int` |
+| `checkposition` | `bit [not null, default: 0]` |
+| `note` | `nvarchar(2000)` |
+| `completedmessage` | `varchar(128)` |
+| `activatedmessage` | `varchar(128)` |
+| `artifacttype` | `int` |
+| `teleportchannel` | `int` |
+| `npcpresenceid` | `int` |
+| `targetorder` | `int [not null, default: 0]` |
+| `displayorder` | `int [not null, default: 0]` |
+| `branchmissionid` | `int` |
+| `optional` | `bit [not null, default: 0]` |
+| `hidden` | `bit [not null, default: 0]` |
+| `structureeid` | `bigint` |
+| `primarydefinitionfromindex` | `int` |
+| `secondarydefinitionfromindex` | `int` |
+| `findradius` | `int` |
+| `spawnnpcs` | `bit [not null, default: 0]` |
+| `snaptonextstructure` | `bit [not null, default: 0]` |
+| `generatesecondarydefinition` | `bit [not null, default: 0]` |
+| `targetsecondaryasmyprimary` | `bit [not null, default: 0]` |
+| `targetprimaryasmysecondary` | `bit [not null, default: 0]` |
+| `anylocation` | `bit [not null, default: 0]` |
+| `usequantityonly` | `bit [not null, default: 0]` |
+| `generateresearchkit` | `bit [not null, default: 0]` |
+| `generatecprg` | `bit [not null, default: 0]` |
+| `primarycategory` | `bigint` |
+| `secondarycategory` | `bigint` |
+| `secondaryquantity` | `int` |
+| `scaleprimaryqwithlevel` | `bit [not null, default: 0]` |
+| `scalesecondaryqwithlevel` | `bit [not null, default: 0]` |
+| `primaryscalemult` | `float` |
+| `secondaryscalemult` | `float` |
+| `structurename` | `nvarchar(128)` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiontargets"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `missiontargettypes.id`
+
+---
+
+## missiontargetsarchive
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `missionid` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `targetid` | `int [not null]` |
+| `progresscount` | `int [not null]` |
+| `completed` | `bit [not null, default: 0]` |
+| `missionguid` | `uniqueidentifier [not null]` |
+| `targetorder` | `int` |
+| `displayorder` | `int` |
+| `definition` | `int` |
+| `quantity` | `int` |
+| `structureeid` | `bigint` |
+| `secondarydefinition` | `int` |
+| `secondaryquantity` | `int` |
+| `zoneid` | `int` |
+| `x` | `int` |
+| `y` | `int` |
+| `artifacttype` | `int` |
+| `targettype` | `int` |
+| `scantype` | `int` |
+| `targetrange` | `int` |
+| `successx` | `int` |
+| `successy` | `int` |
+| `successzoneid` | `int` |
+| `successtime` | `datetime` |
+
+### Relations
+
+- Referenced by `missionlog.missionGuid`
+
+---
+
+## missiontargetslog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+| `targettype` | `int [not null]` |
+| `guid` | `uniqueidentifier [not null]` |
+| `locationeid` | `bigint [not null]` |
+| `missioncategory` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiontargetslog"]`
+
+---
+
+## missiontoagent
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `missionid` | `int [not null]` |
+| `agentid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiontoagent"]`
+- `(missionid, agentid) [unique, name: "IX_missiontoagent_unique"]`
+
+---
+
+## missiontolocation
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `missionid` | `int [not null]` |
+| `locationid` | `int [not null]` |
+| `attempts` | `int [not null]` |
+| `success` | `int [not null]` |
+| `uniquecases` | `int [not null, default: 0]` |
+| `rewardaverage` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_missiontolocation"]`
+
+---
+
+## modulepropertymodifiers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `categoryflags` | `bigint [not null]` |
+| `basefield` | `int [not null]` |
+| `modifierfield` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_modulepropertymodifiers"]`
+
+---
+
+## mtproductprices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `productkey` | `varchar(50) [not null]` |
+| `price` | `int [not null]` |
+
+---
+
+## newscategories
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `category` | `nvarchar(2000)` |
+
+### Indexes
+
+- `id [pk, name: "PK_newscategories"]`
+
+### Relations
+
+- `id` → `news.type`
+
+---
+
+## news
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `idx` | `"int IDENTITY(1,1)" [not null]` |
+| `title` | `nvarchar(128) [not null]` |
+| `body` | `nvarchar(4000) [not null]` |
+| `ntime` | `smalldatetime [not null, default: `getdate()`]` |
+| `type` | `int [not null, default: 0]` |
+| `language` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `idx [pk, name: "PK_news"]`
+
+### Relations
+
+- Referenced by `newscategories.id`
+
+---
+
+## npcbossinfo
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `flockid` | `int [not null]` |
+| `respawnNoiseFactor` | `float` |
+| `lootSplitFlag` | `bit [not null]` |
+| `outpostEID` | `bigint` |
+| `stabilityPts` | `int` |
+| `overrideRelations` | `bit [not null]` |
+| `customDeathMessage` | `varchar(128)` |
+| `customAggressMessage` | `varchar(128)` |
+| `riftConfigId` | `int` |
+| `isAnnounced` | `bit [not null, default: 0]` |
+| `isServerWideAnnouncement` | `bit` |
+| `isNoRadioDelay` | `bit` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## npccontaineritems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `lootdefinition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `probability` | `float [not null]` |
+| `repackaged` | `bit [not null]` |
+| `dontdamage` | `bit [not null]` |
+| `minquantity` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npccontaineritems"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## npcescalactions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `presenceId` | `int [not null]` |
+| `flockId` | `int [not null]` |
+| `level` | `int [not null]` |
+| `chance` | `float [not null, default: 1.0]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## npcspawn
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `description` | `varchar(50)` |
+| `note` | `nvarchar(2000)` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcspawn"]`
+- `name [unique, name: "IX_npcspawn"]`
+
+### Relations
+
+- `id` → `npcpresence.spawnid`
+
+---
+
+## npcpresence
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `topx` | `int [not null, default: 0]` |
+| `topy` | `int [not null, default: 0]` |
+| `bottomx` | `int [not null, default: 0]` |
+| `bottomy` | `int [not null, default: 0]` |
+| `note` | `nvarchar(2000)` |
+| `spawnid` | `int` |
+| `enabled` | `bit [not null, default: 1]` |
+| `roaming` | `bit [not null, default: 0]` |
+| `roamingrespawnseconds` | `int [not null, default: 0]` |
+| `presencetype` | `int [not null, default: 0]` |
+| `maxrandomflock` | `int` |
+| `randomcenterx` | `int` |
+| `randomcentery` | `int` |
+| `randomradius` | `int` |
+| `dynamiclifetime` | `int` |
+| `isbodypull` | `bit [not null, default: 1]` |
+| `isrespawnallowed` | `bit [not null, default: 1]` |
+| `safebodypull` | `bit [not null, default: 0]` |
+| `izgroupid` | `int` |
+| `growthseconds` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcpresence"]`
+- `name [unique, name: "IX_npcpresence_name"]`
+
+### Relations
+
+- Referenced by `npcspawn.id`
+- `id` → `npcflock.presenceid`
+
+---
+
+## npcflock
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `presenceid` | `int [not null]` |
+| `flockmembercount` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `spawnoriginX` | `int [not null, default: 0]` |
+| `spawnoriginY` | `int [not null, default: 0]` |
+| `spawnrangeMin` | `int [not null, default: 0]` |
+| `spawnrangeMax` | `int [not null, default: 10]` |
+| `respawnseconds` | `int [not null, default: 1]` |
+| `totalspawncount` | `int [not null, default: 0]` |
+| `homerange` | `int [not null, default: 70]` |
+| `note` | `nvarchar(2000)` |
+| `respawnmultiplierlow` | `float [not null, default: 0.9]` |
+| `enabled` | `bit [not null, default: 1]` |
+| `iscallforhelp` | `bit [not null, default: 1]` |
+| `behaviorType` | `int [not null, default: 1]` |
+| `npcSpecialType` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcflock"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `npcpresence.id`
+- `id` → `npcflockloot.flockid`
+- `id` → `npcpoolpresetvalues.flockid`
+
+---
+
+## npcflockloot
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `flockid` | `int [not null]` |
+| `lootdefinition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `probability` | `float [not null, default: 1]` |
+| `repackaged` | `bit [not null, default: 0]` |
+| `dontdamage` | `bit [not null, default: 0]` |
+| `minquantity` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcflockloot"]`
+- `(lootdefinition, flockid) [unique, name: "IX_npcflockloot"]`
+
+### Relations
+
+- Referenced by `npcflock.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## npcinterzonegroup
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(64) [not null]` |
+| `respawnTime` | `int [not null, default: 86400]` |
+| `respawnNoiseFactor` | `float [not null, default: 0.15]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## npckills
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `amount` | `int [not null, default: 1]` |
+
+---
+
+## npcloot
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `lootdefinition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `probability` | `float [not null, default: 1]` |
+| `repackaged` | `bit [not null, default: 0]` |
+| `dontdamage` | `bit [not null, default: 0]` |
+| `minquantity` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcloot"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## npcpoolpresets
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcpoolpresets"]`
+
+### Relations
+
+- `id` → `npcpoolpresetvalues.presetid`
+
+---
+
+## npcpoolpresetvalues
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `presetid` | `int [not null]` |
+| `flockid` | `int [not null]` |
+| `rate` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcpoolpresetvalues"]`
+
+### Relations
+
+- Referenced by `npcflock.id`
+- Referenced by `npcpoolpresets.id`
+
+---
+
+## npcrandomflockpool
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `presenceid` | `int [not null]` |
+| `flockid` | `int [not null]` |
+| `rate` | `float [not null, default: 0]` |
+| `lastwave` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcpresenceflockrelation"]`
+
+---
+
+## npcreinforcements
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `reinforcementType` | `int [not null]` |
+| `targetId` | `int [not null]` |
+| `threshold` | `float [not null]` |
+| `presenceId` | `int [not null]` |
+| `zoneId` | `int` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## npcreinforcementtypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(64) [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## npcsafespawnpoints
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_npcsafespawnpoints"]`
+
+---
+
+## npcSpecialTypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(64) [not null]` |
+| `[value]` | `int [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## nspools
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_nspools"]`
+
+### Relations
+
+- `id` → `nspoolmembers.poolid`
+- `id` → `nspoolrelation.sourcepool`
+- `id` → `nspoolrelation.targetpool`
+
+---
+
+## nspoolmembers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `poolid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_nspoolmembers"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `nspools.id`
+
+---
+
+## nspoolrelation
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `sourcepool` | `int [not null]` |
+| `targetpool` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_nspoolrelation"]`
+
+### Relations
+
+- Referenced by `nspools.id`
+- Referenced by `nspools.id`
+
+---
+
+## nstemplates
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `field` | `int [not null]` |
+| `worstvalue` | `float [not null]` |
+| `bestvalue` | `float [not null]` |
+| `note` | `nvarchar(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_aggregatevaluesrandomconfig"]`
+
+### Relations
+
+- Referenced by `aggregatefields.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## opp_reimburselog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `Id` | `"int IDENTITY(1,1)" [not null]` |
+| `ReimburseTo` | `int [not null]` |
+| `ReimburseBy` | `int [not null]` |
+| `ReimburseTime` | `datetime [not null]` |
+| `EntityId` | `int [not null]` |
+| `ItemType` | `nvarchar(16) [not null]` |
+| `Qty` | `int [not null]` |
+
+---
+
+## ownerincome
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporationeid` | `bigint [not null]` |
+| `amount` | `float [not null]` |
+| `lastflush` | `smalldatetime [not null, default: `getdate()`]` |
+
+---
+
+## packageitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `packageid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_packageitems"]`
+
+### Relations
+
+- Referenced by `packages.id`
+
+---
+
+## passablemappoints
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_passablemappoints"]`
+
+---
+
+## paymentproducts
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `productname` | `varchar(256) [not null]` |
+| `priceUSD` | `float [not null, default: 0]` |
+| `priceEUR` | `float [not null, default: 0]` |
+| `note` | `nvarchar(2000)` |
+| `available` | `bit [not null, default: 0]` |
+| `hash` | `int [not null, default: 0]` |
+| `priceFormerEUR` | `float` |
+| `priceFormerUSD` | `float` |
+| `timespan` | `int [not null, default: 0]` |
+| `recurring` | `int [default: 0]` |
+| `aws_Sku` | `varchar(64)` |
+| `visible` | `bit [not null, default: 0]` |
+| `trialonly` | `bit [not null, default: 0]` |
+| `ingame` | `bit [not null, default: 0]` |
+| `displayorder` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_paymentproducts"]`
+- `productname [unique, name: "IX_paymentproducts"]`
+- `hash [unique, name: "IX_paymentproducts_hash"]`
+
+---
+
+## paypal_transactions_history
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `ID` | `"int IDENTITY(1,1)" [not null]` |
+| `transactionID` | `nvarchar(32) [not null]` |
+| `transactionType` | `nvarchar(50) [not null]` |
+| `paymentType` | `nvarchar(50) [not null]` |
+| `orderTime` | `datetime [not null]` |
+| `amt` | `float [not null]` |
+| `currencyCode` | `nvarchar(3) [not null]` |
+| `feeAmt` | `float [not null]` |
+| `settleAmt` | `float [not null, default: 0]` |
+| `taxAmt` | `float [not null]` |
+| `exchangeRate` | `float [default: 1]` |
+| `paymentStatus` | `nvarchar(50)` |
+| `pendingReason` | `nvarchar(50)` |
+| `reasonCode` | `nvarchar(50)` |
+| `orderID` | `int [not null]` |
+
+### Indexes
+
+- `ID [pk, name: "PK_paypal_transactions_history"]`
+
+---
+
+## pbsconnections
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `sourceeid` | `bigint [not null]` |
+| `targeteid` | `bigint [not null]` |
+| `weight` | `float [not null, default: 1.0]` |
+
+---
+
+## pbslog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `corporationeid` | `bigint [not null]` |
+| `nodeeid` | `bigint [not null]` |
+| `nodedefinition` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `eventtype` | `int [not null]` |
+| `issuercharacterid` | `int` |
+| `takeovercorporationeid` | `bigint` |
+| `othernodeeid` | `bigint` |
+| `othernodedefinition` | `int` |
+| `materialdefinition` | `int` |
+| `materialamount` | `int` |
+| `zoneid` | `int` |
+| `killercharacterid` | `int` |
+| `reinforcecounter` | `int` |
+
+---
+
+## pbsregisteredmembers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `characterid` | `int [not null]` |
+
+---
+
+## pbsreimburse
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `corporationeid` | `bigint` |
+| `baseeid` | `bigint [not null]` |
+
+---
+
+## pbstrash
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `baseeid` | `bigint [not null]` |
+| `waskilled` | `bit [not null, default: 0]` |
+| `note` | `nvarchar(2048)` |
+
+---
+
+## plantdamagetype
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `plantdamagetype` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_plantdamagetype"]`
+- `definition [unique, name: "IX_plantdamagetype"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## plantrules
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `idx` | `"int IDENTITY(1,1)" [not null]` |
+| `plantrule` | `varchar(256) [not null]` |
+| `rulesetid` | `int [not null, default: 0]` |
+| `note` | `nvarchar(1024)` |
+
+### Indexes
+
+- `idx [pk, name: "PK_plantrules"]`
+- `(plantrule, rulesetid) [unique, name: "IX_plantrules"]`
+
+---
+
+## plasma_gathered
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `gathered_on` | `date [not null]` |
+| `plasma_type` | `varchar(100) [not null]` |
+| `quantity` | `bigint [not null]` |
+
+---
+
+## plasma_gathered_daily
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `gathered_on` | `date [not null]` |
+| `plasma_type` | `varchar(100) [not null]` |
+| `quantity` | `bigint [not null]` |
+
+---
+
+## plasma_sold
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `sold_on` | `date [not null]` |
+| `plasma_type` | `varchar(100) [not null]` |
+| `quantity` | `bigint [not null]` |
+| `income` | `float` |
+
+---
+
+## polls
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `pollid` | `"int IDENTITY(1,1)" [not null]` |
+| `topic` | `nvarchar(MAX) [not null]` |
+| `participation` | `int [not null, default: 55]` |
+| `active` | `bit [not null, default: 1]` |
+| `started` | `smalldatetime [not null, default: `getdate()`]` |
+| `ended` | `smalldatetime` |
+
+### Indexes
+
+- `pollid [pk, name: "PK_polls"]`
+
+### Relations
+
+- `pollid` → `pollanswers.pollid`
+- `pollid` → `pollchoices.pollid`
+
+---
+
+## pollanswers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `pollid` | `int [not null]` |
+| `accountid` | `int [not null]` |
+| `answerid` | `int [not null]` |
+
+### Relations
+
+- Referenced by `polls.pollid`
+
+---
+
+## pollchoices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `pollid` | `int [not null]` |
+| `choiceid` | `"int IDENTITY(1,1)" [not null]` |
+| `choicetext` | `nvarchar(MAX) [not null]` |
+
+### Indexes
+
+- `(pollid, choiceid) [unique, name: "IX_pollchoices"]`
+
+### Relations
+
+- Referenced by `polls.pollid`
+
+---
+
+## premadechatmessage
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `int [not null]` |
+| `name` | `varchar(32) [not null]` |
+| `message` | `nvarchar(2000) [not null]` |
+
+### Indexes
+
+- `id [pk]`
+- `name [unique]`
+
+---
+
+## premademail
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `int [not null]` |
+| `name` | `varchar(32) [not null]` |
+| `subject` | `nvarchar(128) [not null]` |
+| `body` | `nvarchar(2000) [not null]` |
+
+### Indexes
+
+- `id [pk]`
+- `name [unique]`
+
+---
+
+## productioncost
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `category` | `bigint` |
+| `tiertype` | `int` |
+| `tierlevel` | `int` |
+| `costmodifier` | `float [not null, default: 1.0]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## productiondecalibration
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `categoryflag` | `bigint [not null]` |
+| `distorsionmin` | `float [not null]` |
+| `distorsionmax` | `float [not null]` |
+| `decrease` | `float` |
+
+### Indexes
+
+- `id [pk, name: "PK_productiondecalibration_1"]`
+- `categoryflag [unique, name: "IX_productiondecalibration_1"]`
+
+### Relations
+
+- Referenced by `categoryFlags.[value]`
+
+---
+
+## productionduration
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `category` | `bigint [not null]` |
+| `durationmodifier` | `float [not null, default: 1]` |
+
+### Indexes
+
+- `category [unique, name: "IX_productionduration"]`
+
+### Relations
+
+- Referenced by `categoryFlags.[value]`
+
+---
+
+## productionlines
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `facilityeid` | `bigint [not null]` |
+| `runningproductionid` | `int` |
+| `targetdefinition` | `int [not null]` |
+| `materialefficiency` | `float [not null, default: 0.5]` |
+| `timeefficiency` | `float [not null, default: 0.5]` |
+| `cycles` | `int [not null, default: 0]` |
+| `rounds` | `int [not null, default: 1]` |
+| `cprgeid` | `bigint` |
+
+### Indexes
+
+- `id [pk, name: "PK_productionlines"]`
+
+---
+
+## productionlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `amount` | `int [not null]` |
+| `productiontime` | `datetime [not null, default: `getdate()`]` |
+| `productiontype` | `int [not null]` |
+| `durationsecs` | `int [not null, default: 0]` |
+| `price` | `float [not null, default: 0]` |
+| `usecorporationwallet` | `bit [not null, default: 0]` |
+
+---
+
+## prototypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `prototype` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_prototypes"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## rarematerials
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `raredefinition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `chance` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_rarematerials"]`
+
+---
+
+## raw_material_prices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `material_name` | `varchar(100) [not null]` |
+| `price_nic` | `decimal(18,2) [not null]` |
+
+### Indexes
+
+- `material_name [pk]`
+
+---
+
+## reimbursementlog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `targetaccountid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `comment` | `varchar(512)` |
+| `wasinsured` | `bit [not null, default: 0]` |
+| `killedbyplayer` | `bit [not null, default: 0]` |
+| `inactiveperiod` | `int [not null, default: 0]` |
+| `dctime` | `datetime` |
+| `deleted` | `datetime` |
+
+---
+
+## relays
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `relayname` | `varchar(50) [not null]` |
+| `maxusers` | `int [not null]` |
+| `currentusers` | `int [not null, default: 0]` |
+| `ipaddress` | `varchar(32) [not null]` |
+| `port` | `int [not null]` |
+
+---
+
+## relicloot
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `minquantity` | `int [not null]` |
+| `maxquantity` | `int [not null]` |
+| `chance` | `decimal(9,6) [not null]` |
+| `relictypeid` | `int [not null]` |
+| `packed` | `bit [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## relicspawninfo
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `relictypeid` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `rate` | `int [not null]` |
+| `x` | `int` |
+| `y` | `int` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## relictypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `raceid` | `int` |
+| `level` | `int` |
+| `ep` | `int` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## reliczoneconfig
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `maxspawn` | `int [not null]` |
+| `respawnrate` | `int [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## resource_market_prices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `calculated_on` | `date [not null]` |
+| `resource_name` | `varchar(100) [not null]` |
+| `unit_price` | `decimal(18,2) [not null]` |
+
+---
+
+## resources_gathered
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `gathered_on` | `date [not null]` |
+| `resource_name` | `varchar(100) [not null]` |
+| `quantity` | `bigint [not null]` |
+
+---
+
+## resources_gathered_daily
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `gathered_on` | `date [not null]` |
+| `resource_name` | `varchar(100) [not null]` |
+| `quantity` | `bigint [not null]` |
+
+---
+
+## riftconfigs
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(100) [not null]` |
+| `destinationGroupId` | `int` |
+| `lifespanSeconds` | `int` |
+| `maxUses` | `int` |
+| `categoryExclusionGroupId` | `int` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## riftdestinations
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `groupId` | `int [not null]` |
+| `zoneId` | `int [not null]` |
+| `x` | `int` |
+| `y` | `int` |
+| `weight` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## robotassembler
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `charactereid` | `bigint [not null]` |
+| `facilityeid` | `bigint [not null]` |
+| `head` | `bigint` |
+| `chassis` | `bigint` |
+| `leg` | `bigint` |
+
+### Indexes
+
+- `(characterid, facilityeid) [unique, name: "IX_robotassembler"]`
+
+---
+
+## robotfittingpresets
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `ownerEid` | `bigint [not null]` |
+| `preset` | `varchar(MAX) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_robotpresets"]`
+
+---
+
+## robotsavedeffects
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `owner` | `bigint [not null]` |
+| `effects` | `text` |
+
+---
+
+## robotsetup
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `robotshell` | `int [not null]` |
+| `head` | `int [not null]` |
+| `chassis` | `int [not null]` |
+| `leg` | `int [not null]` |
+| `container` | `int [not null]` |
+| `hybridshell` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_robotsetup"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## robottemplates
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `description` | `varchar(MAX) [not null]` |
+| `note` | `nvarchar(2000)` |
+
+### Indexes
+
+- `id [pk, name: "PK_robottemplates"]`
+- `name [unique, name: "IX_robottemplates_name"]`
+
+### Relations
+
+- `id` → `robottemplaterelation.templateid`
+
+---
+
+## robottemplaterelation
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `definition` | `int [not null]` |
+| `templateid` | `int [not null]` |
+| `itemscoresum` | `int [not null, default: 0]` |
+| `raceid` | `int [not null, default: 0]` |
+| `missionlevel` | `int` |
+| `missionleveloverride` | `int` |
+| `killep` | `int` |
+| `note` | `varchar(256)` |
+
+### Indexes
+
+- `definition [pk, name: "PK_robottemplaterelation"]`
+- `(definition, templateid) [unique, name: "IX_robottemplaterelation"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `robottemplates.id`
+
+---
+
+## runningproduction
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterID` | `int [not null]` |
+| `characterEID` | `bigint [not null]` |
+| `resultDefinition` | `int [not null]` |
+| `type` | `int [not null]` |
+| `startTime` | `datetime [not null]` |
+| `finishTime` | `datetime [not null]` |
+| `facilityEID` | `bigint [not null]` |
+| `totalProductionTime` | `int [not null]` |
+| `baseEID` | `bigint [not null]` |
+| `creditTaken` | `float [not null]` |
+| `pricePerSecond` | `float [not null]` |
+| `licenseAmount` | `int [not null, default: 0]` |
+| `amountOfCycles` | `int [not null, default: 0]` |
+| `useCorporationWallet` | `bit [not null]` |
+| `paused` | `bit [not null, default: 0]` |
+| `pausetime` | `datetime` |
+
+### Indexes
+
+- `id [pk, name: "PK_runningproduction"]`
+
+---
+
+## runningproductionreserveditem
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `runningid` | `int [not null]` |
+| `reservedEID` | `bigint [not null]` |
+
+---
+
+## savedeffects
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `effects` | `varchar(MAX) [not null]` |
+
+---
+
+## season_activity_rates
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `season_id` | `int [not null]` |
+| `activity_type` | `int [not null]` |
+| `points_per_unit` | `float [not null]` |
+| `unit_scale` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_season_activity_rates"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+
+---
+
+## season_character_points
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `character_id` | `int [not null]` |
+| `season_id` | `int [not null]` |
+| `total_points` | `float [not null, default: 0]` |
+| `last_updated` | `datetime [not null, default: \`getutcdate()\`]` |
+| `intro_mail_sent` | `bit [not null, default: 0]` |
+| `leaderboard_reward_delivered` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `character_id, season_id [pk, name: "PK_season_character_points"]`
+- `season_id, total_points [name: "IX_season_character_points_season"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+
+---
+
+## season_leaderboard_rewards
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `season_id` | `int [not null]` |
+| `rank_min` | `int [not null]` |
+| `rank_max` | `int [not null]` |
+| `package_id` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_season_leaderboard_rewards"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+
+---
+
+## season_objective_progress
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `character_id` | `int [not null]` |
+| `season_id` | `int [not null]` |
+| `objective_id` | `int [not null]` |
+| `current_value` | `float [not null, default: 0]` |
+| `completed` | `bit [not null, default: 0]` |
+| `completed_time` | `datetime` |
+| `bonus_awarded` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `character_id, season_id, objective_id [pk, name: "PK_season_objective_progress"]`
+- `character_id, season_id [name: "IX_season_objective_progress_char"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+- `objective_id` → `season_objectives.id`
+
+---
+
+## season_objectives
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `season_id` | `int [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `description` | `varchar(512) [not null, default: '']` |
+| `activity_type` | `int [not null]` |
+| `target_value` | `bigint [not null]` |
+| `bonus_points` | `int [not null]` |
+| `display_order` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_season_objectives"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+- `id` → `season_objective_progress.objective_id`
+
+---
+
+## season_tier_claims
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `character_id` | `int [not null]` |
+| `season_id` | `int [not null]` |
+| `tier_id` | `int [not null]` |
+| `claimed_time` | `datetime [not null, default: \`getutcdate()\`]` |
+
+### Indexes
+
+- `character_id, season_id, tier_id [pk, name: "PK_season_tier_claims"]`
+- `character_id, season_id [name: "IX_season_tier_claims_char"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+- `tier_id` → `season_tiers.id`
+
+---
+
+## season_tiers
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `season_id` | `int [not null]` |
+| `tier_number` | `int [not null]` |
+| `tier_name` | `varchar(64) [not null]` |
+| `points_required` | `int [not null]` |
+| `package_id` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_season_tiers"]`
+
+### Relations
+
+- `season_id` → `seasons.id`
+- `id` → `season_tier_claims.tier_id`
+
+---
+
+## seasons
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+| `description` | `varchar(512) [not null, default: '']` |
+| `start_time` | `datetime [not null]` |
+| `end_time` | `datetime [not null]` |
+| `is_active` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_seasons"]`
+
+### Relations
+
+- `id` → `season_activity_rates.season_id`
+- `id` → `season_objectives.season_id`
+- `id` → `season_tiers.season_id`
+- `id` → `season_leaderboard_rewards.season_id`
+- `id` → `season_character_points.season_id`
+- `id` → `season_objective_progress.season_id`
+- `id` → `season_tier_claims.season_id`
+
+---
+
+## serverinfo
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `name` | `nvarchar(512)` |
+| `description` | `nvarchar(2048)` |
+| `contact` | `nvarchar(512)` |
+| `isopen` | `bit [not null, default: 0]` |
+| `isbroadcast` | `bit [not null, default: 0]` |
+
+---
+
+## settings
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `varkey` | `varchar(64) [not null]` |
+| `varvalue` | `varchar(512) [not null]` |
+| `notes` | `varchar(1024)` |
+
+### Indexes
+
+- `varkey [pk, name: "PK_settings"]`
+
+---
+
+## siegeitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `minquantity` | `int [not null]` |
+| `maxquantity` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_siegeitemchains"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+
+---
+
+## slotFlags
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `offset` | `int [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `note` | `nvarchar(2048)` |
+
+---
+
+## sparks
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `sparkname` | `varchar(128) [not null]` |
+| `unlockprice` | `int` |
+| `energycredit` | `int` |
+| `standinglimit` | `float` |
+| `definition` | `int` |
+| `quantity` | `int` |
+| `changeprice` | `int [not null, default: 0]` |
+| `displayorder` | `int [not null]` |
+| `defaultspark` | `bit [not null, default: 0]` |
+| `icon` | `varchar(128)` |
+| `hidden` | `bit [not null, default: 1]` |
+| `note` | `nvarchar(1024)` |
+| `allianceeid` | `bigint` |
+| `alliancename` | `varchar(128)` |
+| `unlockable` | `bit [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_sparks"]`
+- `sparkname [unique, name: "IX_sparks"]`
+
+### Relations
+
+- `id` → `sparkextensions.sparkid`
+
+---
+
+## sparkextensions
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `sparkid` | `int [not null]` |
+| `extensionid` | `int [not null]` |
+| `extensionlevel` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_sparkextensions"]`
+
+### Relations
+
+- Referenced by `extensions.extensionid`
+- Referenced by `sparks.id`
+
+---
+
+## standinglog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `characterid` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `actual` | `float [not null]` |
+| `change` | `float [not null]` |
+| `allianceeid` | `bigint [not null]` |
+| `missionid` | `int` |
+
+---
+
+## standings
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `source` | `bigint [not null]` |
+| `target` | `bigint [not null]` |
+| `standing` | `float [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_standings"]`
+- `(source, target) [unique, name: "IX_standings"]`
+
+---
+
+## steamkeys
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `accountid` | `int` |
+| `steamid` | `varchar(64)` |
+| `steamkey` | `varchar(32) [not null]` |
+| `assigned` | `datetime` |
+
+### Indexes
+
+- `id [pk, name: "PK_steamkeys"]`
+
+---
+
+## steamkeyscomp
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `steamkey` | `varchar(32) [not null]` |
+| `givenaway` | `date` |
+| `note` | `nvarchar(2048)` |
+
+### Indexes
+
+- `id [pk, name: "PK_steamkeyscomp"]`
+
+---
+
+## storecategories
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(128) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_shopcategories"]`
+
+### Relations
+
+- `id` → `storeitems.category`
+
+---
+
+## storeitems
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null, default: 1]` |
+| `price` | `int [not null]` |
+| `category` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_shopitems"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `storecategories.id`
+
+---
+
+## strongholdexitconfig
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+| `riftConfigId` | `int` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## techline
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `note` | `nvarchar(2000)` |
+
+### Indexes
+
+- `id [pk, name: "PK_techline"]`
+
+### Relations
+
+- `id` → `techlineincrement.techlineid`
+- `id` → `techlinemember.techlineid`
+
+---
+
+## techlineincrement
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `techlineid` | `int [not null]` |
+| `multiplier` | `float [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_techlineincrement"]`
+- `(definition, techlineid) [unique, name: "IX_techlineincrement"]`
+
+### Relations
+
+- Referenced by `techline.id`
+- Referenced by `entitydefaults.definition`
+
+---
+
+## techlinemember
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `techlineid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `position` | `int [not null]` |
+| `points` | `float [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_techlinemember"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `techline.id`
+
+---
+
+## techtree
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `parentdefinition` | `int [not null]` |
+| `childdefinition` | `int [not null]` |
+| `groupID` | `int [not null, default: 0]` |
+| `x` | `int [not null, default: 0]` |
+| `y` | `int [not null, default: 0]` |
+| `enablerextensionid` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_techtree"]`
+- `(parentdefinition, childdefinition) [unique, name: "IX_parentchild"]`
+
+---
+
+## techtreegroups
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(64) [not null]` |
+| `enablerextensionid` | `int` |
+| `displayOrder` | `int` |
+
+### Indexes
+
+- `id [pk, name: "PK_techtreegroups"]`
+
+---
+
+## techtreelog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `logType` | `int [not null]` |
+| `character` | `int [not null]` |
+| `corporationEid` | `bigint` |
+| `definition` | `int [not null]` |
+| `quantity` | `int [not null]` |
+| `pointType` | `int [not null]` |
+| `amount` | `int [not null]` |
+| `created` | `datetime [not null, default: `getdate()`]` |
+
+### Indexes
+
+- `id [pk, name: "PK_techtreelog"]`
+
+---
+
+## techtreenodeprices
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `pointtype` | `int [not null]` |
+| `amount` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_techtreepoints"]`
+
+---
+
+## techtreepoints
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `owner` | `bigint [not null]` |
+| `pointtype` | `int [not null]` |
+| `amount` | `int [not null]` |
+
+---
+
+## techtreepointtypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(64) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_techtreepointtypes"]`
+
+---
+
+## techtreeunlockednodes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `definition` | `int [not null]` |
+| `owner` | `bigint [not null]` |
+| `created` | `datetime [not null, default: `getdate()`]` |
+
+### Indexes
+
+- `id [pk, name: "PK_techtreeunlockeddefinitions"]`
+
+---
+
+## terraformprojectregistration
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `projectid` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `role` | `int [not null]` |
+
+### Indexes
+
+- `(projectid, characterid) [unique, name: "IX_terraformprojectregistration_1"]`
+
+---
+
+## terraformprojects
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `title` | `nvarchar(512)` |
+| `ownercharacterid` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+| `topx` | `int [not null]` |
+| `topy` | `int [not null]` |
+| `bottomx` | `int [not null]` |
+| `bottomy` | `int [not null]` |
+| `version` | `int [not null, default: 0]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `lastmodified` | `datetime [not null, default: `getdate()`]` |
+| `validuntil` | `datetime [not null, default: `getdate()`]` |
+| `data` | `varbinary(MAX)` |
+
+### Indexes
+
+- `id [pk, name: "PK_terraformprojects"]`
+
+---
+
+## tiertypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50)` |
+
+### Indexes
+
+- `id [pk, name: "PK_tiertypes"]`
+
+---
+
+## traceips
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `ip` | `varchar(50) [not null]` |
+| `name` | `varchar(1024)` |
+
+### Indexes
+
+- `id [pk, name: "PK_traceips"]`
+
+---
+
+## traceroutelog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `accountid` | `int [not null]` |
+| `sessionguid` | `varchar(64) [not null]` |
+| `ip` | `varchar(32)` |
+| `step` | `int [not null]` |
+| `ipstatus` | `int [not null]` |
+| `roundtriptime` | `bigint [not null]` |
+| `tracetime` | `datetime [not null, default: `getdate()`]` |
+| `fromclient` | `bit [not null, default: 0]` |
+| `country` | `varchar(128)` |
+| `tracedipid` | `int [not null, default: 0]` |
+
+---
+
+## trainingartifacts
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `artifactType` | `int [not null]` |
+| `x` | `int [not null]` |
+| `y` | `int [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_trainingartifacts"]`
+
+---
+
+## trainingrewards
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `level` | `int [not null, default: 0]` |
+| `definition` | `int` |
+| `quantity` | `int` |
+| `robottemplateid` | `int` |
+| `raceid` | `int [not null, default: 1]` |
+
+### Indexes
+
+- `id [pk, name: "PK_trainingrewards"]`
+
+---
+
+## transactiontypes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `[value]` | `int [not null]` |
+| `name` | `varchar(256) [not null]` |
+
+### Indexes
+
+- `"[value]" [pk, name: "PK_transactiontypes"]`
+
+---
+
+## transportassignments
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `creation` | `datetime [not null, default: `getdate()`]` |
+| `sourcebaseeid` | `bigint [not null]` |
+| `targetbaseeid` | `bigint [not null]` |
+| `ownercharacterid` | `int [not null]` |
+| `reward` | `bigint [not null]` |
+| `collateral` | `bigint [not null]` |
+| `taken` | `bit [not null, default: 0]` |
+| `volunteercharacterid` | `int` |
+| `containereid` | `bigint [not null]` |
+| `containername` | `varchar(10) [not null]` |
+| `volume` | `float [not null]` |
+| `expiry` | `datetime [not null]` |
+| `started` | `datetime` |
+| `retrieved` | `bit [not null, default: 0]` |
+
+### Indexes
+
+- `id [pk, name: "PK_transportassignments"]`
+
+---
+
+## transportassignmentslog
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `assignmentevent` | `int [not null]` |
+| `baseeid` | `bigint [not null]` |
+| `ownercharacterid` | `int [not null]` |
+| `volunteercharacterid` | `int` |
+| `assignmentid` | `int [not null]` |
+| `containername` | `varchar(10) [not null]` |
+
+---
+
+## transportassignmenttimes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `characterid` | `int [not null]` |
+| `eventtime` | `datetime [not null, default: `getdate()`]` |
+| `sourcebase` | `bigint [not null]` |
+| `targetbase` | `bigint [not null]` |
+| `distance` | `float [not null]` |
+| `volume` | `float [not null]` |
+| `totalseconds` | `float [not null]` |
+| `multiplier` | `float [not null]` |
+
+---
+
+## usercount
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `time` | `datetime [not null, default: `getdate()`]` |
+| `usercount` | `int [not null]` |
+
+---
+
+## vendorpresets
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `nvarchar(50) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_vendorpresets"]`
+
+### Relations
+
+- `id` → `vendorpresetvalues.presetid`
+
+---
+
+## vendorpresetvalues
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `idx` | `"int IDENTITY(1,1)" [not null]` |
+| `presetid` | `int [not null]` |
+| `definition` | `int [not null]` |
+| `issell` | `bit [not null, default: 1]` |
+| `price` | `float [not null, default: 10]` |
+| `quantity` | `int [not null, default: 1]` |
+| `duration` | `int [not null]` |
+
+### Indexes
+
+- `idx [pk, name: "PK_vendorpresetvalues"]`
+
+### Relations
+
+- Referenced by `entitydefaults.definition`
+- Referenced by `vendorpresets.id`
+
+---
+
+## vendors
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `vendorEID` | `bigint [not null]` |
+| `marketEID` | `bigint` |
+| `vendorsellprofit` | `float [not null, default: 1]` |
+| `vendorbuyprofit` | `float [not null, default: 1]` |
+| `note` | `nchar(2048)` |
+
+### Indexes
+
+- `(vendorEID, marketEID) [unique, name: "IX_vendors"]`
+
+---
+
+## votes
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `voteid` | `"int IDENTITY(1,1)" [not null]` |
+| `groupEID` | `bigint [not null]` |
+| `votename` | `nvarchar(50) [not null]` |
+| `votetopic` | `nvarchar(2048)` |
+| `participation` | `int [not null, default: 1]` |
+| `votetype` | `int [not null, default: 0]` |
+| `closed` | `bit [not null, default: 0]` |
+| `startdate` | `smalldatetime [not null, default: `getdate()`]` |
+| `enddate` | `smalldatetime` |
+| `result` | `bit` |
+| `startedby` | `int [not null]` |
+| `consensusrate` | `int [not null, default: 50]` |
+
+### Indexes
+
+- `voteid [pk, name: "PK_votes_1"]`
+
+### Relations
+
+- `voteid` → `voteentries.voteid`
+
+---
+
+## voteentries
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `voteid` | `int [not null]` |
+| `characterid` | `int [not null]` |
+| `voteentry` | `bit [not null]` |
+| `entrydate` | `smalldatetime [not null, default: `getdate()`]` |
+
+### Relations
+
+- Referenced by `votes.voteid`
+
+---
+
+## yellowpages
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `ID` | `"int IDENTITY(1,1)" [not null]` |
+| `corporationEID` | `bigint [not null]` |
+| `primaryActivity` | `int [not null, default: 0]` |
+| `zoneID` | `int` |
+| `baseEID` | `bigint` |
+| `orientation` | `int [not null, default: 0]` |
+| `lookingFor` | `int [not null, default: 0]` |
+| `preferredFaction` | `int` |
+| `providesInsurance` | `int [not null, default: 0]` |
+| `timeZone` | `int [not null, default: 0]` |
+| `requiredActivity` | `int [not null, default: 0]` |
+| `communication` | `int [not null, default: 0]` |
+| `services` | `int [not null, default: 0]` |
+
+### Indexes
+
+- `ID [pk, name: "PK_yellowpages"]`
+- `corporationEID [unique, name: "IX_yellowpages"]`
+
+---
+
+## zoneeffects
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `effectid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## zoneentities
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneID` | `int [not null]` |
+| `eid` | `bigint` |
+| `definition` | `int` |
+| `owner` | `bigint` |
+| `ename` | `varchar(128)` |
+| `x` | `float [not null]` |
+| `y` | `float [not null]` |
+| `z` | `float [not null]` |
+| `orientation` | `tinyint [not null, default: 0]` |
+| `enabled` | `bit [not null, default: 1]` |
+| `note` | `nvarchar(2000)` |
+| `runtime` | `bit [not null, default: 0]` |
+| `synckey` | `varchar(50)` |
+
+### Indexes
+
+- `id [pk, name: "PK_zoneentities"]`
+- `eid [unique, name: "IX_zoneentities_eid_uk"]`
+
+### Relations
+
+- Referenced by `zones.id`
+
+---
+
+## zoneriftsconfig
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `zoneid` | `int [not null]` |
+| `maxrifts` | `int [not null]` |
+| `maxlevel` | `int [not null]` |
+
+### Indexes
+
+- `id [pk]`
+- `zoneid [unique]`
+
+---
+
+## zonesectors
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `name` | `varchar(50) [not null]` |
+| `zoneid` | `int [not null]` |
+| `sector` | `varbinary(512) [not null]` |
+
+### Indexes
+
+- `id [pk, name: "PK_zonesectors"]`
+- `name [unique, name: "IX_zonesectors_name"]`
+
+### Relations
+
+- Referenced by `zones.id`
+
+---
+
+## zoneteleportdevicemap
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `id` | `"int IDENTITY(1,1)" [not null]` |
+| `sourcedefinition` | `int [not null]` |
+| `zoneid` | `int [not null]` |
+
+### Indexes
+
+- `id [pk]`
+
+---
+
+## zoneuserentities
+
+**Schema:** `dbo`
+
+### Columns
+
+| Column | Definition |
+|---|---|
+| `eid` | `bigint [not null]` |
+| `zoneid` | `int [not null]` |
+| `x` | `float [not null]` |
+| `y` | `float [not null]` |
+| `z` | `float [not null]` |
+| `orientation` | `tinyint [not null, default: 0]` |
+
+### Indexes
+
+- `eid [pk, name: "PK_zoneuserentities"]`
+
+---
diff --git a/docs/db_structure/functions/CFName.sql b/docs/db_structure/functions/CFName.sql
new file mode 100644
index 0000000..e61607b
--- /dev/null
+++ b/docs/db_structure/functions/CFName.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[CFName] Script Date: 10.05.2026 10:19:38 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[CFName]
+(
+ @definition int
+)
+RETURNS VARCHAR(50)
+AS
+BEGIN
+
+ DECLARE @result VARCHAR(50)
+ DECLARE @cfValue BIGINT
+ SELECT @cfValue = ed.categoryflags FROM dbo.entitydefaults ed WHERE ed.definition=@definition;
+ SELECT @result = cf.name FROM dbo.categoryFlags cf WHERE cf.value = @cfValue;
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/DynpropRemoveKey.sql b/docs/db_structure/functions/DynpropRemoveKey.sql
new file mode 100644
index 0000000..34ecf03
--- /dev/null
+++ b/docs/db_structure/functions/DynpropRemoveKey.sql
@@ -0,0 +1,46 @@
+/****** Object: UserDefinedFunction [dbo].[DynpropRemoveKey] Script Date: 10.05.2026 10:25:04 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[DynpropRemoveKey]
+(
+ @origDynprop VARCHAR(MAX),
+ @keyString VARCHAR(4096)
+)
+RETURNS VARCHAR(MAX)
+AS
+BEGIN
+
+DECLARE @firstIndex INT,@secondIndex INT,@result VARCHAR(MAX),@tmpS VARCHAR(4096),@patternS VARCHAR(4096)
+
+SET @patternS = '#' + @keyString
+
+SELECT @firstIndex= CHARINDEX(@patternS,@origDynprop)
+
+SELECT @secondIndex= CHARINDEX('#',@origDynprop,@firstIndex)
+
+IF (@firstIndex=0)
+ BEGIN
+ RETURN @origDynprop
+ END
+
+
+IF (@secondIndex=0)
+ BEGIN
+ SET @result = SUBSTRING(@origDynprop,@firstIndex-1,0)
+ END
+ELSE
+ BEGIN
+ SET @tmpS = SUBSTRING(@origDynprop,@firstIndex,@secondIndex)
+ SET @result = REPLACE(@origDynprop,@tmpS,'')
+ END
+
+
+ RETURN @result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetCEO.sql b/docs/db_structure/functions/GetCEO.sql
new file mode 100644
index 0000000..767895a
--- /dev/null
+++ b/docs/db_structure/functions/GetCEO.sql
@@ -0,0 +1,28 @@
+/****** Object: UserDefinedFunction [dbo].[GetCEO] Script Date: 10.05.2026 10:33:42 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+--mamlasz, bena, balfasz. corprole az itt hardkodolt, majd valami...
+
+
+create FUNCTION [dbo].[GetCEO]
+(
+ @corpEID bigint
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result int
+
+ SELECT @Result = (select memberid from corporationmembers where corporationeid=@corpEID and ([role] & 1) = 1)
+
+ RETURN @Result
+
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetCFMask.sql b/docs/db_structure/functions/GetCFMask.sql
new file mode 100644
index 0000000..a596141
--- /dev/null
+++ b/docs/db_structure/functions/GetCFMask.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[GetCFMask] Script Date: 10.05.2026 10:34:13 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[GetCFMask]
+(
+
+ @cf bigint
+)
+RETURNS bigint
+AS
+BEGIN
+
+DECLARE @mask bigint
+SET @mask = 0
+
+WHILE (@cf > 0)
+BEGIN
+ SET @cf = @cf / 256
+ SET @mask = @mask * 256
+ SET @mask = @mask | 255
+END
+RETURN @mask
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetCorporationName.sql b/docs/db_structure/functions/GetCorporationName.sql
new file mode 100644
index 0000000..1b45a59
--- /dev/null
+++ b/docs/db_structure/functions/GetCorporationName.sql
@@ -0,0 +1,26 @@
+/****** Object: UserDefinedFunction [dbo].[GetCorporationName] Script Date: 10.05.2026 10:35:57 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[GetCorporationName]
+(
+ @corporationEID bigint
+)
+RETURNS VARCHAR(64)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(64)
+
+
+ SELECT @Result = (SELECT [NAME] FROM corporations WHERE eid=@corporationEID)
+
+ -- Return the result of the function
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetDefinitionName.sql b/docs/db_structure/functions/GetDefinitionName.sql
new file mode 100644
index 0000000..80bf4b3
--- /dev/null
+++ b/docs/db_structure/functions/GetDefinitionName.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[GetDefinitionName] Script Date: 10.05.2026 10:39:15 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[GetDefinitionName]
+(
+
+ @definition int
+)
+RETURNS VARCHAR(128)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(128)
+
+
+ SELECT @Result = (SELECT definitionname FROM dbo.entitydefaults WHERE definition=@definition)
+
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetNick.sql b/docs/db_structure/functions/GetNick.sql
new file mode 100644
index 0000000..7ed771c
--- /dev/null
+++ b/docs/db_structure/functions/GetNick.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[GetNick] Script Date: 10.05.2026 10:41:32 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[GetNick]
+(
+
+ @characterId int
+)
+RETURNS VARCHAR(128)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(128)
+
+
+ SELECT @Result = (SELECT nick FROM characters WHERE characterid=@characterId)
+
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetPublicContainerEidByBaseEid.sql b/docs/db_structure/functions/GetPublicContainerEidByBaseEid.sql
new file mode 100644
index 0000000..3397903
--- /dev/null
+++ b/docs/db_structure/functions/GetPublicContainerEidByBaseEid.sql
@@ -0,0 +1,23 @@
+/****** Object: UserDefinedFunction [dbo].[GetPublicContainerEidByBaseEid] Script Date: 10.05.2026 10:43:23 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[GetPublicContainerEidByBaseEid]
+(
+ @baseEid bigint
+)
+RETURNS bigint
+AS
+BEGIN
+ DECLARE @containerEid bigint
+
+ SET @containerEid = (SELECT TOP 1 eid FROM dbo.entities WHERE parent=@baseEid AND definition=166)
+
+ RETURN @containerEid
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetRandomEid.sql b/docs/db_structure/functions/GetRandomEid.sql
new file mode 100644
index 0000000..6192390
--- /dev/null
+++ b/docs/db_structure/functions/GetRandomEid.sql
@@ -0,0 +1,31 @@
+/****** Object: UserDefinedFunction [dbo].[GetRandomEid] Script Date: 10.05.2026 10:43:54 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[GetRandomEid]
+(
+)
+RETURNS bigint
+AS
+BEGIN
+
+ DECLARE @Result BIGINT, @bmax BIGINT, @bmin BIGINT , @diff BIGINT, @ftmp FLOAT
+
+ SET @bmax = 576460752303423487
+ SET @bmin = 8589934591
+ SET @diff = @bmax - @bmin
+
+ SET @ftmp = ((SELECT TOP 1 * FROM dbo.randomView) * @diff) + @bmin
+
+
+ SET @Result = (CAST(@ftmp AS BIGINT))
+
+
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetTableStats.sql b/docs/db_structure/functions/GetTableStats.sql
new file mode 100644
index 0000000..519885b
--- /dev/null
+++ b/docs/db_structure/functions/GetTableStats.sql
@@ -0,0 +1,23 @@
+/****** Object: UserDefinedFunction [dbo].[GetTableStats] Script Date: 10.05.2026 10:05:01 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[GetTableStats]
+(
+ @tableName VARCHAR(128)
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT name AS stats_name,
+ STATS_DATE(object_id, stats_id) AS updtime
+FROM sys.stats
+WHERE object_id = OBJECT_ID( REPLACE(@tableName,'dbo.',''))
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GetTransactionTypeName.sql b/docs/db_structure/functions/GetTransactionTypeName.sql
new file mode 100644
index 0000000..bcc99d1
--- /dev/null
+++ b/docs/db_structure/functions/GetTransactionTypeName.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[GetTransactionTypeName] Script Date: 10.05.2026 10:44:28 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+create FUNCTION [dbo].[GetTransactionTypeName]
+(
+
+ @enumvalue int
+)
+RETURNS VARCHAR(128)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(128)
+
+ SELECT @Result = (SELECT name FROM dbo.transactiontypes WHERE value=@enumvalue)
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/GuidToUid.sql b/docs/db_structure/functions/GuidToUid.sql
new file mode 100644
index 0000000..c784a41
--- /dev/null
+++ b/docs/db_structure/functions/GuidToUid.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[GuidToUid] Script Date: 10.05.2026 10:46:30 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+-- =============================================
+-- Author:
+-- Create date:
+-- Description:
+-- =============================================
+CREATE FUNCTION [dbo].[GuidToUid]
+(
+ @guid as uniqueidentifier
+)
+RETURNS bigint
+AS
+BEGIN
+ RETURN abs(cast(cast(@guid as varbinary(16)) as bigint))
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/IsCampaignEligible.sql b/docs/db_structure/functions/IsCampaignEligible.sql
new file mode 100644
index 0000000..2c4adae
--- /dev/null
+++ b/docs/db_structure/functions/IsCampaignEligible.sql
@@ -0,0 +1,32 @@
+/****** Object: UserDefinedFunction [dbo].[IsCampaignEligible] Script Date: 10.05.2026 10:49:07 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[IsCampaignEligible]
+(
+ @accountID int,
+ @campaignToken VARCHAR(128)
+)
+RETURNS int
+AS
+BEGIN
+ DECLARE @campaignID INT
+
+ SET @campaignID = (SELECT id FROM dbo.campaigns WHERE campaigntoken=@campaignToken)
+
+ IF EXISTS (SELECT accountid FROM dbo.accountcampaignitems WHERE accountid=@accountID AND campaignid=@campaignID)
+ BEGIN
+ RETURN 0
+ END
+ ELSE
+ BEGIN
+ RETURN 1
+ END
+
+ RETURN 0
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/IsDefinitionRepackable.sql b/docs/db_structure/functions/IsDefinitionRepackable.sql
new file mode 100644
index 0000000..573aeb0
--- /dev/null
+++ b/docs/db_structure/functions/IsDefinitionRepackable.sql
@@ -0,0 +1,34 @@
+/****** Object: UserDefinedFunction [dbo].[IsDefinitionRepackable] Script Date: 10.05.2026 10:49:42 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+-- is repackable?
+-- !AlwaysStackable && !NonStackable
+
+
+CREATE FUNCTION [dbo].[IsDefinitionRepackable]
+(
+ @definition int
+)
+RETURNS BIT
+AS
+BEGIN
+ DECLARE @attributeFlags bigint
+
+ SET @attributeFlags = (SELECT attributeflags FROM dbo.entitydefaults WHERE [definition]=@definition)
+
+ IF ((@attributeFlags & 3072) = 0)
+ BEGIN
+ RETURN 1
+ END
+
+ RETURN 0
+
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/ToHex.sql b/docs/db_structure/functions/ToHex.sql
new file mode 100644
index 0000000..c0cdecf
--- /dev/null
+++ b/docs/db_structure/functions/ToHex.sql
@@ -0,0 +1,29 @@
+/****** Object: UserDefinedFunction [dbo].[ToHex] Script Date: 10.05.2026 10:57:35 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[ToHex](@value int)
+RETURNS varchar(50)
+AS
+BEGIN
+ DECLARE @seq char(16)
+ DECLARE @result varchar(50)
+ DECLARE @digit char(1)
+ SET @seq = '0123456789ABCDEF'
+
+ SET @result = SUBSTRING(@seq, (@value%16)+1, 1)
+
+ WHILE @value > 0
+ BEGIN
+ SET @digit = SUBSTRING(@seq, ((@value/16)%16)+1, 1)
+
+ SET @value = @value/16
+ IF @value <> 0 SET @result = @digit + @result
+ END
+
+ RETURN @result
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/TryGetRandomEid.sql b/docs/db_structure/functions/TryGetRandomEid.sql
new file mode 100644
index 0000000..27988d8
--- /dev/null
+++ b/docs/db_structure/functions/TryGetRandomEid.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[TryGetRandomEid] Script Date: 10.05.2026 10:58:18 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[TryGetRandomEid]
+()
+RETURNS bigint
+AS
+BEGIN
+
+ DECLARE @Result bigint
+
+ SET @Result = dbo.GetRandomEid()
+
+ WHILE (SELECT COUNT(*) FROM dbo.entities WHERE eid=@Result) > 0
+ BEGIN
+ SET @Result = dbo.GetRandomEid()
+ END
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/accountPackageHasItem.sql b/docs/db_structure/functions/accountPackageHasItem.sql
new file mode 100644
index 0000000..8f89e3f
--- /dev/null
+++ b/docs/db_structure/functions/accountPackageHasItem.sql
@@ -0,0 +1,66 @@
+/****** Object: UserDefinedFunction [dbo].[accountPackageHasItem] Script Date: 10.05.2026 10:14:27 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[accountPackageHasItem]
+(
+ @accountId INT,
+ @itemDefinition INT,
+ @itemQuantity INT,
+ @packageid INT
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @hasItem INT, @releaseDate DATETIME, @fixDate DATETIME;
+ SET @releaseDate = CAST('2015-12-18 21:00:01.000' AS DATETIME);
+ SET @fixDate = CAST('2015-12-28 16:57:22.717' AS DATETIME);
+
+ DECLARE @xmas2015From DATETIME, @xmas2015To DATETIME;
+ SET @xmas2015From = CAST( '2015-12-25 20:06:45.000' AS DATETIME);
+ SET @xmas2015To = CAST( '2015-12-25 20:06:51.500' AS DATETIME);
+
+
+ -- fixed version, default
+ SET @hasItem = (
+ SELECT COUNT(*) FROM dbo.accountredeemableitems
+ WHERE
+ accountid=@accountId
+ AND [definition]=@itemDefinition
+ AND quantity=@itemQuantity
+ AND creation>@releaseDate
+ AND packageid=@packageId
+ );
+
+ IF (@hasItem > 0)
+ BEGIN
+ RETURN 1;
+ END
+
+ -- bugged period
+ SET @hasItem = (
+ SELECT COUNT(*) FROM dbo.accountredeemableitems
+ WHERE
+ accountid=@accountId
+ AND [definition]=@itemDefinition
+ AND quantity=@itemQuantity
+ AND creation>@releaseDate
+ AND creation<@fixDate
+ AND creation NOT BETWEEN @xmas2015From AND @xmas2015To
+ AND packageid IS NULL
+ );
+
+ IF (@hasItem > 0)
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/accountPackageHasItemTest.sql b/docs/db_structure/functions/accountPackageHasItemTest.sql
new file mode 100644
index 0000000..cd8baf0
--- /dev/null
+++ b/docs/db_structure/functions/accountPackageHasItemTest.sql
@@ -0,0 +1,66 @@
+/****** Object: UserDefinedFunction [dbo].[accountPackageHasItemTest] Script Date: 10.05.2026 10:15:17 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[accountPackageHasItemTest]
+(
+ @accountId INT,
+ @itemDefinition INT,
+ @itemQuantity INT,
+ @packageid INT
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @hasItem INT, @releaseDate DATETIME, @fixDate DATETIME;
+ SET @releaseDate = CAST('2015-12-18 21:00:01.000' AS DATETIME);
+ SET @fixDate = CAST('2015-12-28 16:57:22.717' AS DATETIME);
+
+ DECLARE @xmas2015From DATETIME, @xmas2015To DATETIME;
+ SET @xmas2015From = CAST( '2015-12-25 20:06:45.000' AS DATETIME);
+ SET @xmas2015To = CAST( '2015-12-25 20:06:51.500' AS DATETIME);
+
+
+ -- fixed version, default
+ SET @hasItem = (
+ SELECT COUNT(*) FROM dbo.accountredeemableitems
+ WHERE
+ accountid=@accountId
+ AND [definition]=@itemDefinition
+ AND quantity=@itemQuantity
+ AND creation>@releaseDate
+ AND packageid=@packageId
+ );
+
+ IF (@hasItem > 0)
+ BEGIN
+ RETURN 1;
+ END
+
+ -- bugged period
+ SET @hasItem = (
+ SELECT COUNT(*) FROM dbo.accountredeemableitems
+ WHERE
+ accountid=@accountId
+ AND [definition]=@itemDefinition
+ AND quantity=@itemQuantity
+ AND creation>@releaseDate
+ AND creation<@fixDate
+ AND creation NOT BETWEEN @xmas2015From AND @xmas2015To
+ AND packageid IS NULL
+ );
+
+ IF (@hasItem > 0)
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/accountPackageIsPurchased.sql b/docs/db_structure/functions/accountPackageIsPurchased.sql
new file mode 100644
index 0000000..1badfe6
--- /dev/null
+++ b/docs/db_structure/functions/accountPackageIsPurchased.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[accountPackageIsPurchased] Script Date: 10.05.2026 10:15:54 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[accountPackageIsPurchased]
+(
+ @accountId INT,
+ @packageId INT
+)
+RETURNS bit
+AS
+BEGIN
+
+ DECLARE @hasPack INT;
+ SET @hasPack = (SELECT COUNT(*) FROM dbo.accountpremiumpackages WHERE accountid=@accountId AND packageid=@packageId);
+
+ IF (@hasPack > 0)
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0;
+
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/activityNameByType.sql b/docs/db_structure/functions/activityNameByType.sql
new file mode 100644
index 0000000..a37352e
--- /dev/null
+++ b/docs/db_structure/functions/activityNameByType.sql
@@ -0,0 +1,67 @@
+/****** Object: UserDefinedFunction [dbo].[activityNameByType] Script Date: 10.05.2026 10:16:28 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+/*
+ Undefined =0,
+ Gathering, // mining / harvesting
+ Mission, // any mission objective
+ Production, // any production
+ Artifact,
+ Intrusion,
+ Npc
+
+*/
+
+
+CREATE FUNCTION [dbo].[activityNameByType]
+(
+ @activityType int
+)
+RETURNS VARCHAR(20)
+AS
+BEGIN
+ DECLARE @name VARCHAR(20);
+
+
+
+
+ IF (@activityType =0)
+ BEGIN
+ SET @name ='Undefined';
+ END
+ ELSE IF (@activityType =1)
+ BEGIN
+ SET @name = 'Gathering';
+ END
+ ELSE IF (@activityType =2)
+ BEGIN
+ SET @name = 'Mission';
+ END
+ ELSE IF (@activityType =3)
+ BEGIN
+ SET @name = 'Production';
+ END
+ ELSE IF (@activityType =4)
+ BEGIN
+ SET @name = 'Artifact';
+ END
+ ELSE IF (@activityType =5)
+ BEGIN
+ SET @name = 'Intrusion';
+ END
+ ELSE IF (@activityType =6)
+ BEGIN
+ SET @name = 'Npc';
+ END
+
+
+ RETURN @name;
+
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateFieldsUsed.sql b/docs/db_structure/functions/aggregateFieldsUsed.sql
new file mode 100644
index 0000000..472f66f
--- /dev/null
+++ b/docs/db_structure/functions/aggregateFieldsUsed.sql
@@ -0,0 +1,31 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateFieldsUsed] Script Date: 10.05.2026 9:46:31 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[aggregateFieldsUsed]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT DISTINCT usedFields.field from
+ (
+ SELECT DISTINCT av.field FROM dbo.aggregatevalues av WHERE av.definition IN (SELECT definition FROM dbo.entitydefaults WHERE enabled=1)
+ UNION
+ SELECT DISTINCT mp.basefield FROM dbo.modulepropertymodifiers mp
+ UNION
+ SELECT DISTINCT mp2.modifierfield FROM dbo.modulepropertymodifiers mp2
+ UNION
+ SELECT DISTINCT em.field FROM dbo.effectdefaultmodifiers em
+ UNION
+ SELECT DISTINCT ex.targetpropertyID FROM dbo.extensions ex
+ ) usedFields
+
+)
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateInfoByCFString.sql b/docs/db_structure/functions/aggregateInfoByCFString.sql
new file mode 100644
index 0000000..3c3c7d2
--- /dev/null
+++ b/docs/db_structure/functions/aggregateInfoByCFString.sql
@@ -0,0 +1,76 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateInfoByCFString] Script Date: 10.05.2026 9:47:14 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[aggregateInfoByCFString]
+(
+ @cfString VARCHAR(128),
+ @marginWorst FLOAT,
+ @marginBest FLOAT
+
+)
+RETURNS
+@result TABLE
+(
+
+ field INT,
+ fieldname NVARCHAR(100),
+ increasing VARCHAR(5),
+ average FLOAT,
+ worstvalue FLOAT,
+ bestvalue FLOAT,
+ worstmargin FLOAT,
+ bestmargin FLOAT,
+ serie VARCHAR(max),
+ mindef VARCHAR(100),
+ maxdef VARCHAR(100),
+ defserie VARCHAR(max)
+)
+AS
+BEGIN
+
+DECLARE @definitions IntList;
+ INSERT @definitions ( idval )
+ SELECT definition FROM dbo.getDefinitionByCFString(@cfString);
+
+
+ INSERT @result
+ ( field ,
+ fieldname ,
+ increasing,
+ average ,
+ worstvalue ,
+ bestvalue ,
+ worstmargin,
+ bestmargin,
+ serie,
+ mindef,
+ maxdef,
+ defserie
+ )
+ SELECT ar.field,ar.fieldname,ar.increasing,
+ ROUND(AVG(ar.value),2),
+ dbo.bestWorstValues(@definitions, ar.field,0,0,0),
+ dbo.bestWorstValues(@definitions, ar.field,1,0,0),
+ dbo.bestWorstValues(@definitions, ar.field,0, @marginWorst, @marginBest),
+ dbo.bestWorstValues(@definitions, ar.field,1, @marginWorst, @marginBest),
+ dbo.aggregateValueSeries(@definitions, ar.field),
+ dbo.extremeDefsByCfField(@definitions, ar.field, 0),
+ dbo.extremeDefsByCfField(@definitions, ar.field, 1),
+ dbo.defNameSeries(@definitions, ar.field)
+ FROM dbo.aggregateRecordsByIds(@definitions) ar
+ GROUP BY ar.field,ar.fieldname,ar.increasing
+
+
+ RETURN
+END
+
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateInfoByDefList.sql b/docs/db_structure/functions/aggregateInfoByDefList.sql
new file mode 100644
index 0000000..0337e7f
--- /dev/null
+++ b/docs/db_structure/functions/aggregateInfoByDefList.sql
@@ -0,0 +1,78 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateInfoByDefList] Script Date: 10.05.2026 9:48:00 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[aggregateInfoByDefList]
+(
+ @defString VARCHAR(max),
+ @delimiter VARCHAR(10),
+ @marginWorst FLOAT,
+ @marginBest FLOAT
+
+)
+RETURNS
+@result TABLE
+(
+
+ field INT,
+ fieldname NVARCHAR(100),
+ increasing VARCHAR(5),
+ average FLOAT,
+ worstvalue FLOAT,
+ bestvalue FLOAT,
+ worstmargin FLOAT,
+ bestmargin FLOAT,
+ serie VARCHAR(max),
+ mindef VARCHAR(100),
+ maxdef VARCHAR(100),
+ defserie VARCHAR(max)
+)
+AS
+BEGIN
+
+
+ DECLARE @definitions IntList;
+ INSERT @definitions ( idval )
+ SELECT CAST( LTRIM(RTRIM(value)) AS INT) FROM dbo.splitString(@defString, @delimiter);
+
+
+ INSERT @result
+ ( field ,
+ fieldname ,
+ increasing,
+ average ,
+ worstvalue ,
+ bestvalue ,
+ worstmargin,
+ bestmargin,
+ serie,
+ mindef,
+ maxdef,
+ defserie
+ )
+ SELECT ar.field,ar.fieldname,ar.increasing,
+ ROUND(AVG(ar.value),2),
+ dbo.bestWorstValues(@definitions, ar.field,0,0,0),
+ dbo.bestWorstValues(@definitions, ar.field,1,0,0),
+ dbo.bestWorstValues(@definitions, ar.field,0, @marginWorst, @marginBest),
+ dbo.bestWorstValues(@definitions, ar.field,1, @marginWorst, @marginBest),
+ dbo.aggregateValueSeries(@definitions, ar.field),
+ dbo.extremeDefsByCfField(@definitions, ar.field, 0),
+ dbo.extremeDefsByCfField(@definitions, ar.field, 1),
+ dbo.defNameSeries(@definitions, ar.field)
+ FROM dbo.aggregateRecordsByIds(@definitions) ar
+ GROUP BY ar.field,ar.fieldname,ar.increasing
+
+
+ RETURN
+END
+
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateInfoByIds.sql b/docs/db_structure/functions/aggregateInfoByIds.sql
new file mode 100644
index 0000000..52c7b9a
--- /dev/null
+++ b/docs/db_structure/functions/aggregateInfoByIds.sql
@@ -0,0 +1,71 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateInfoByIds] Script Date: 10.05.2026 9:48:50 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[aggregateInfoByIds]
+(
+ @definitions IntList READONLY,
+ @marginWorst FLOAT,
+ @marginBest FLOAT
+
+)
+RETURNS
+@result TABLE
+(
+
+ field INT,
+ fieldname NVARCHAR(100),
+ increasing VARCHAR(5),
+ average FLOAT,
+ worstvalue FLOAT,
+ bestvalue FLOAT,
+ worstmargin FLOAT,
+ bestmargin FLOAT,
+ serie VARCHAR(max),
+ mindef VARCHAR(100),
+ maxdef VARCHAR(100),
+ defserie VARCHAR(max)
+)
+AS
+BEGIN
+
+ INSERT @result
+ ( field ,
+ fieldname ,
+ increasing,
+ average ,
+ worstvalue ,
+ bestvalue ,
+ worstmargin,
+ bestmargin,
+ serie,
+ mindef,
+ maxdef,
+ defserie
+ )
+ SELECT ar.field,ar.fieldname,ar.increasing,
+ ROUND(AVG(ar.value),2),
+ dbo.bestWorstValues(@definitions, ar.field,0,0,0),
+ dbo.bestWorstValues(@definitions, ar.field,1,0,0),
+ dbo.bestWorstValues(@definitions, ar.field,0, @marginWorst, @marginBest),
+ dbo.bestWorstValues(@definitions, ar.field,1, @marginWorst, @marginBest),
+ dbo.aggregateValueSeries(@definitions, ar.field),
+ dbo.extremeDefsByCfField(@definitions, ar.field, 0),
+ dbo.extremeDefsByCfField(@definitions, ar.field, 1),
+ dbo.defNameSeries(@definitions, ar.field)
+ FROM dbo.aggregateRecordsByIds(@definitions) ar
+ GROUP BY ar.field,ar.fieldname,ar.increasing
+
+
+ RETURN
+END
+
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateRecordsByCfString.sql b/docs/db_structure/functions/aggregateRecordsByCfString.sql
new file mode 100644
index 0000000..5996b00
--- /dev/null
+++ b/docs/db_structure/functions/aggregateRecordsByCfString.sql
@@ -0,0 +1,37 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateRecordsByCfString] Script Date: 10.05.2026 9:49:36 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[aggregateRecordsByCfString]
+(
+ @cfString VARCHAR(128)
+)
+RETURNS
+@result TABLE
+(
+ [definition] INT,
+ [field] INT,
+ [value] FLOAT,
+ [definitionname] VARCHAR(100),
+ [fieldname] nvarchar(100),
+ [moreisbetter] BIT,
+ [increasing] VARCHAR(5)
+)
+AS
+BEGIN
+
+ DECLARE @definitions IntList;
+ INSERT @definitions ( idval )
+ SELECT definition FROM dbo.getDefinitionByCFString(@cfString);
+
+ INSERT @result
+ ( [definition], [field], [value], [definitionname], [fieldname], moreisbetter, increasing )
+ SELECT r.[definition], r.[field],r.[value], r.[definitionname],r.[fieldname],r.[moreisbetter],r.[increasing] FROM dbo.aggregateRecordsByIds(@definitions) r;
+
+ RETURN
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateRecordsByDefList.sql b/docs/db_structure/functions/aggregateRecordsByDefList.sql
new file mode 100644
index 0000000..783bb63
--- /dev/null
+++ b/docs/db_structure/functions/aggregateRecordsByDefList.sql
@@ -0,0 +1,38 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateRecordsByDefList] Script Date: 10.05.2026 9:50:12 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[aggregateRecordsByDefList]
+(
+ @defString VARCHAR(max),
+ @delimiter VARCHAR(10)
+)
+RETURNS
+@result TABLE
+(
+ [definition] INT,
+ [field] INT,
+ [value] FLOAT,
+ [definitionname] VARCHAR(100),
+ [fieldname] nvarchar(100),
+ [moreisbetter] BIT,
+ [increasing] VARCHAR(5)
+)
+AS
+BEGIN
+
+ DECLARE @definitions IntList;
+ INSERT @definitions ( idval )
+ SELECT CAST( LTRIM(RTRIM(value)) AS INT) FROM dbo.splitString(@defString, @delimiter);
+
+ INSERT @result
+ ( [definition], [field], [value], [definitionname], [fieldname], moreisbetter, increasing )
+ SELECT r.[definition], r.[field],r.[value], r.[definitionname],r.[fieldname],r.[moreisbetter],r.[increasing] FROM dbo.aggregateRecordsByIds(@definitions) r;
+
+ RETURN
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateRecordsByIds.sql b/docs/db_structure/functions/aggregateRecordsByIds.sql
new file mode 100644
index 0000000..950be74
--- /dev/null
+++ b/docs/db_structure/functions/aggregateRecordsByIds.sql
@@ -0,0 +1,42 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateRecordsByIds] Script Date: 10.05.2026 9:50:44 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+-- core - eats intlist
+CREATE FUNCTION [dbo].[aggregateRecordsByIds]
+(
+ @definitions IntList READONLY
+)
+RETURNS
+@result TABLE
+(
+ [definition] INT,
+ [field] INT,
+ [value] FLOAT,
+ [definitionname] VARCHAR(100),
+ [fieldname] nvarchar(100),
+ [moreisbetter] BIT,
+ [increasing] VARCHAR(5)
+)
+AS
+BEGIN
+ INSERT @result
+ ( [definition], [field], [value], [definitionname], [fieldname], moreisbetter, increasing )
+
+ SELECT av.[definition],av.[field],av.[value],ed.definitionname,fl.name,fl.moreisbetter,dbo.isAggregateFieldMoreIsBetter(av.field)
+ FROM dbo.aggregatevalues av
+JOIN dbo.entitydefaults ed ON ed.definition = av.definition
+JOIN dbo.aggregatefields fl ON fl.id = av.field
+WHERE av.definition IN (
+SELECT idval FROM @definitions )
+AND ed.enabled =1
+AND fl.usedinconfig =1
+ RETURN
+END
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/aggregateValueSeries.sql b/docs/db_structure/functions/aggregateValueSeries.sql
new file mode 100644
index 0000000..81ca154
--- /dev/null
+++ b/docs/db_structure/functions/aggregateValueSeries.sql
@@ -0,0 +1,69 @@
+/****** Object: UserDefinedFunction [dbo].[aggregateValueSeries] Script Date: 10.05.2026 10:17:03 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[aggregateValueSeries]
+(
+ @definitions IntList READONLY,
+ @fieldId INT
+
+
+)
+RETURNS VARCHAR(max)
+AS
+BEGIN
+
+
+DECLARE @delimiter VARCHAR(5) = ' ';
+DECLARE @result VARCHAR(max);
+SET @result = '';
+DECLARE @cVal FLOAT;
+
+DECLARE @moreIsBetter BIT;
+SELECT @moreIsBetter=moreisbetter FROM dbo.aggregatefields WHERE id=@fieldId;
+
+DECLARE @valz CURSOR;
+
+IF (@moreIsBetter = 0)
+BEGIN
+ -- decreasing: smaller value means better performance
+
+ SET @valz = CURSOR LOCAL READ_ONLY FAST_FORWARD FORWARD_ONLY FOR
+ SELECT ar.value FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId ORDER BY ar.value DESC;
+
+END
+ELSE
+BEGIN
+
+ -- inscreasing: larger value means better performance
+
+ SET @valz = CURSOR LOCAL READ_ONLY FAST_FORWARD FORWARD_ONLY FOR
+ SELECT ar.value FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId ORDER BY ar.value ASC;
+
+
+END
+
+OPEN @valz; FETCH NEXT FROM @valz INTO @cVal;
+WHILE (@@FETCH_STATUS =0)
+BEGIN
+ -- 123 spaces
+ SET @result = @result + CAST(@cVal AS varchar(30)) + @delimiter + ' ';
+
+FETCH NEXT FROM @valz INTO @cVal; END; CLOSE @valz;DEALLOCATE @valz;
+
+
+SET @result = SUBSTRING(@result, 0 , LEN(@result) - LEN(@delimiter) +1);
+
+
+RETURN @result;
+
+END
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/allCodingObjects.sql b/docs/db_structure/functions/allCodingObjects.sql
new file mode 100644
index 0000000..c9b8d12
--- /dev/null
+++ b/docs/db_structure/functions/allCodingObjects.sql
@@ -0,0 +1,23 @@
+/****** Object: UserDefinedFunction [dbo].[allCodingObjects] Script Date: 10.05.2026 9:51:26 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[allCodingObjects]
+(
+
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT a.object_id,a.schema_id, a.Name AS name
+FROM sys.objects a
+INNER JOIN sys.schemas b
+ON a.schema_id = b.schema_id
+WHERE TYPE in ('FN', 'IF', 'TF','AF', 'P' ,'V')
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/allPossibleOwners.sql b/docs/db_structure/functions/allPossibleOwners.sql
new file mode 100644
index 0000000..08d8013
--- /dev/null
+++ b/docs/db_structure/functions/allPossibleOwners.sql
@@ -0,0 +1,21 @@
+/****** Object: UserDefinedFunction [dbo].[allPossibleOwners] Script Date: 10.05.2026 9:52:01 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[allPossibleOwners]
+( )
+RETURNS TABLE
+AS
+RETURN
+(
+
+SELECT eid FROM dbo.corporations
+UNION
+SELECT rooteid FROM dbo.characters WHERE rootEID > 0
+)
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/bestWorstValues.sql b/docs/db_structure/functions/bestWorstValues.sql
new file mode 100644
index 0000000..f41cbb9
--- /dev/null
+++ b/docs/db_structure/functions/bestWorstValues.sql
@@ -0,0 +1,58 @@
+/****** Object: UserDefinedFunction [dbo].[bestWorstValues] Script Date: 10.05.2026 10:17:43 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[bestWorstValues]
+(
+ @definitions IntList READONLY,
+ @fieldId INT,
+ @returnBest BIT,
+ @marginWorst FLOAT,
+ @marginBest FLOAT
+)
+RETURNS float
+AS
+BEGIN
+
+DECLARE @moreIsBetter BIT;
+SELECT @moreIsBetter=moreisbetter FROM dbo.aggregatefields WHERE id=@fieldId;
+
+DECLARE @worstValue FLOAT, @bestValue FLOAT;
+
+IF (@moreIsBetter=0)
+BEGIN
+ --decreasing -> smaller the better
+ SELECT
+ @worstValue = (1+@marginWorst) * MAX(ar.value),
+ @bestValue = (1-@marginBest) * MIN(ar.value)
+ FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId;
+
+END
+ELSE
+BEGIN
+ --increasing -> larger the better
+ SELECT
+ @worstValue = (1-@marginWorst) * MIN(ar.value),
+ @bestValue = (1+@marginBest) * MAX(ar.value)
+ FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId;
+
+END
+
+IF (@returnBest=0)
+BEGIN
+ RETURN @worstValue;
+END
+ RETURN @bestValue;
+
+END
+
+
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/blend.sql b/docs/db_structure/functions/blend.sql
new file mode 100644
index 0000000..78c23bf
--- /dev/null
+++ b/docs/db_structure/functions/blend.sql
@@ -0,0 +1,26 @@
+/****** Object: UserDefinedFunction [dbo].[blend] Script Date: 10.05.2026 10:18:22 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[blend]
+(
+ @bias FLOAT,
+ @valueA FLOAT,
+ @valueB FLOAT
+)
+RETURNS FLOAT
+AS
+BEGIN
+ DECLARE @res FLOAT;
+
+ DECLARE @diff FLOAT;
+ SET @diff = @valueB - @valueA;
+ SET @res = @valueA + (@diff * @bias);
+
+ RETURN @res;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/calcPrice.sql b/docs/db_structure/functions/calcPrice.sql
new file mode 100644
index 0000000..7f2dec7
--- /dev/null
+++ b/docs/db_structure/functions/calcPrice.sql
@@ -0,0 +1,79 @@
+/****** Object: UserDefinedFunction [dbo].[calcPrice] Script Date: 10.05.2026 10:18:58 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+--Calculates a price for a definition
+CREATE FUNCTION [dbo].[calcPrice]
+(
+ -- Add the parameters for the function here
+ @definition int
+)
+RETURNS float
+AS
+BEGIN
+
+ DECLARE @PRate FLOAT, @isManualPrice BIT, @hasComponents BIT, @tmpPrice float
+
+ SET @PRate = (SELECT profitrate FROM dbo.itemprices WHERE definition=@definition)
+
+ IF @PRate IS NULL
+ BEGIN
+ SET @PRate = 0
+ END
+
+ if (select count(*) from components where definition=@definition) = 0
+ BEGIN
+ SET @hasComponents = 0
+ END
+ ELSE
+ BEGIN
+ SET @hasComponents = 1
+ END
+
+ SET @isManualPrice = (SELECT manualprice FROM dbo.itemprices WHERE definition=@definition)
+
+ IF @isManualPrice IS NULL
+ BEGIN
+ SET @isManualPrice = 0
+ END
+
+ -- if the item has components
+ IF (@hasComponents = 1 AND @isManualPrice = 0)
+ BEGIN
+ set @tmpPrice =
+ (select sum(c.componentamount*p.price) from components as c
+ join itemprices as p on p.definition=c.componentdefinition
+ where c.definition=@definition
+ AND c.componentdefinition NOT IN (SELECT definition FROM dbo.entitydefaults WHERE (categoryFlags & 0xffff) = 0x0414 ))
+ END
+ ELSE
+ BEGIN
+ SET @tmpPrice = 1
+ END
+
+ IF (@isManualPrice = 1)
+ BEGIN
+ SET @PRate = 1 --manualprice not scaled by the profit rate
+ SET @tmpPrice = (SELECT price FROM dbo.itemprices WHERE definition=@definition)
+
+ IF @tmpPrice IS NULL
+ BEGIN
+ SET @tmpPrice = 0
+ END
+
+ END
+
+ RETURN @tmpPrice * @PRate
+
+
+END
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/childrenCount.sql b/docs/db_structure/functions/childrenCount.sql
new file mode 100644
index 0000000..e039f2d
--- /dev/null
+++ b/docs/db_structure/functions/childrenCount.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[childrenCount] Script Date: 10.05.2026 10:20:18 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[childrenCount]
+(
+ -- Add the parameters for the function here
+ @eid bigint
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result int
+
+
+ set @result = (SELECT count(*) from entities where parent=@eid)
+
+ -- Return the result of the function
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/counticeingame.sql b/docs/db_structure/functions/counticeingame.sql
new file mode 100644
index 0000000..c39a520
--- /dev/null
+++ b/docs/db_structure/functions/counticeingame.sql
@@ -0,0 +1,21 @@
+/****** Object: UserDefinedFunction [dbo].[counticeingame] Script Date: 10.05.2026 10:20:52 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[counticeingame]
+(
+
+)
+RETURNS int
+AS
+BEGIN
+
+
+ RETURN (SELECT SUM(quantity) FROM entities WHERE definition=5202)
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/countpurchasedice.sql b/docs/db_structure/functions/countpurchasedice.sql
new file mode 100644
index 0000000..e7eac81
--- /dev/null
+++ b/docs/db_structure/functions/countpurchasedice.sql
@@ -0,0 +1,20 @@
+/****** Object: UserDefinedFunction [dbo].[countpurchasedice] Script Date: 10.05.2026 10:21:23 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[countpurchasedice]
+(
+
+)
+RETURNS int
+AS
+BEGIN
+
+ RETURN (SELECT SUM(quantity) FROM accountredeemableitems WHERE definition=5202)
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/countredeemedice.sql b/docs/db_structure/functions/countredeemedice.sql
new file mode 100644
index 0000000..fc93112
--- /dev/null
+++ b/docs/db_structure/functions/countredeemedice.sql
@@ -0,0 +1,21 @@
+/****** Object: UserDefinedFunction [dbo].[countredeemedice] Script Date: 10.05.2026 10:21:59 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[countredeemedice]
+(
+
+)
+RETURNS int
+AS
+BEGIN
+
+
+ RETURN (SELECT SUM(quantity) FROM accountredeemableitems WHERE definition=5202 AND wasredeemed=1)
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/creditOrEpByDefinition.sql b/docs/db_structure/functions/creditOrEpByDefinition.sql
new file mode 100644
index 0000000..e92d2c4
--- /dev/null
+++ b/docs/db_structure/functions/creditOrEpByDefinition.sql
@@ -0,0 +1,64 @@
+/****** Object: UserDefinedFunction [dbo].[creditOrEpByDefinition] Script Date: 10.05.2026 10:22:34 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+/*
+5477 #credit=n200
+5481 #credit=n4800
+5482 #credit=n2500
+
+5475 #ep=n40000
+5479 #ep=n120000
+5480 #ep=n140000
+
+*/
+
+CREATE FUNCTION [dbo].[creditOrEpByDefinition]
+(
+ @definition int
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @result INT;
+ SET @result = 0;
+
+ IF (@definition = 5477 )
+ BEGIN
+ SET @result = 200
+ END
+
+ IF (@definition = 5481 )
+ BEGIN
+ SET @result = 4800
+ END
+
+ IF (@definition = 5482 )
+ BEGIN
+ SET @result = 2500
+ END
+
+ IF (@definition = 5475 )
+ BEGIN
+ SET @result = 40000
+ END
+
+ IF (@definition = 5479 )
+ BEGIN
+ SET @result = 120000
+ END
+
+ IF (@definition = 5480 )
+ BEGIN
+ SET @result = 140000
+ END
+
+ RETURN @result;
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/defNameSeries.sql b/docs/db_structure/functions/defNameSeries.sql
new file mode 100644
index 0000000..339307d
--- /dev/null
+++ b/docs/db_structure/functions/defNameSeries.sql
@@ -0,0 +1,70 @@
+/****** Object: UserDefinedFunction [dbo].[defNameSeries] Script Date: 10.05.2026 10:23:14 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[defNameSeries]
+(
+ @definitions IntList READONLY,
+ @fieldId INT
+
+
+)
+RETURNS VARCHAR(max)
+AS
+BEGIN
+
+
+DECLARE @delimiter VARCHAR(5) = ' ';
+DECLARE @result VARCHAR(max);
+SET @result = '';
+DECLARE @cDef INT;
+
+DECLARE @moreIsBetter BIT;
+SELECT @moreIsBetter=moreisbetter FROM dbo.aggregatefields WHERE id=@fieldId;
+
+DECLARE @valz CURSOR;
+
+IF (@moreIsBetter = 0)
+BEGIN
+ -- decreasing: smaller value means better performance
+
+ SET @valz = CURSOR LOCAL READ_ONLY FAST_FORWARD FORWARD_ONLY FOR
+ SELECT ar.definition FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId ORDER BY ar.value DESC;
+
+END
+ELSE
+BEGIN
+
+ -- inscreasing: larger value means better performance
+
+ SET @valz = CURSOR LOCAL READ_ONLY FAST_FORWARD FORWARD_ONLY FOR
+ SELECT ar.definition FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId ORDER BY ar.value ASC;
+
+
+END
+
+OPEN @valz; FETCH NEXT FROM @valz INTO @cDef;
+WHILE (@@FETCH_STATUS =0)
+BEGIN
+ -- 123 spaces
+ SET @result = @result + dbo.GetDefinitionName(@cDef) + @delimiter + ' ';
+
+FETCH NEXT FROM @valz INTO @cDef; END; CLOSE @valz;DEALLOCATE @valz;
+
+
+SET @result = REPLACE(SUBSTRING(@result, 0 , LEN(@result) - LEN(@delimiter) +1), 'def_','')
+
+
+RETURN @result;
+
+END
+
+
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/distance2d.sql b/docs/db_structure/functions/distance2d.sql
new file mode 100644
index 0000000..626d6eb
--- /dev/null
+++ b/docs/db_structure/functions/distance2d.sql
@@ -0,0 +1,28 @@
+/****** Object: UserDefinedFunction [dbo].[distance2d] Script Date: 10.05.2026 10:23:56 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[distance2d]
+(
+
+ @ox FLOAT,
+ @oy FLOAT,
+ @x FLOAT,
+ @y FLOAT
+)
+RETURNS float
+AS
+BEGIN
+
+ DECLARE @dx FLOAT, @dy FLOAT
+
+ SET @dx = @ox - @x
+ SET @dy = @oy - @y
+
+ RETURN SQRT(@dx*@dx + @dy*@dy)
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/eidToInsertString.sql b/docs/db_structure/functions/eidToInsertString.sql
new file mode 100644
index 0000000..73c5bc9
--- /dev/null
+++ b/docs/db_structure/functions/eidToInsertString.sql
@@ -0,0 +1,41 @@
+/****** Object: UserDefinedFunction [dbo].[eidToInsertString] Script Date: 10.05.2026 10:25:41 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[eidToInsertString]
+(
+ @eid bigint
+)
+RETURNS VARCHAR(MAX)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(MAX)
+
+ SET @Result =
+(
+SELECT
+'('
++ dbo.intToInsertString([eid]) + ', '
++ dbo.intToInsertString([definition]) + ', '
++ dbo.intToInsertString([owner]) + ', '
++ dbo.intToInsertString([parent]) + ', '
++ dbo.intToInsertString([health]) + ', '
++ dbo.stringToInsertString([ename]) + ', '
++ dbo.intToInsertString([quantity]) + ', '
++ dbo.intToInsertString([repackaged]) + ', '
++ dbo.stringToInsertString([dynprop])
++ '),'
+ FROM dbo.entities WHERE eid=@eid
+)
+
+ SET @Result = RTRIM(LTRIM(@Result))
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/emailByAccountId.sql b/docs/db_structure/functions/emailByAccountId.sql
new file mode 100644
index 0000000..4931e28
--- /dev/null
+++ b/docs/db_structure/functions/emailByAccountId.sql
@@ -0,0 +1,21 @@
+/****** Object: UserDefinedFunction [dbo].[emailByAccountId] Script Date: 10.05.2026 10:26:20 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[emailByAccountId]
+(
+ @accountId int
+)
+RETURNS VARCHAR(50)
+AS
+BEGIN
+ DECLARE @email VARCHAR(50);
+ SELECT @email=email FROM dbo.accounts WHERE accountID=@accountId;
+ RETURN @email;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/ewModulesInHex.sql b/docs/db_structure/functions/ewModulesInHex.sql
new file mode 100644
index 0000000..b69ee68
--- /dev/null
+++ b/docs/db_structure/functions/ewModulesInHex.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[ewModulesInHex] Script Date: 10.05.2026 9:52:33 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[ewModulesInHex]
+()
+RETURNS TABLE
+AS
+RETURN
+(
+
+
+SELECT dbo.ToHex(definition) AS hexn from
+(
+SELECT * FROM dbo.getDefinitionByCFString('cf_electronic_warfare_equipment')
+UNION
+SELECT * FROM dbo.getDefinitionByCFString('cf_energy_vampires')
+UNION
+SELECT * FROM dbo.getDefinitionByCFString('cf_energy_neutralizers')
+) AS t
+
+
+
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/extensionPointsAvailable.sql b/docs/db_structure/functions/extensionPointsAvailable.sql
new file mode 100644
index 0000000..598c94b
--- /dev/null
+++ b/docs/db_structure/functions/extensionPointsAvailable.sql
@@ -0,0 +1,31 @@
+/****** Object: UserDefinedFunction [dbo].[extensionPointsAvailable] Script Date: 10.05.2026 10:26:59 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[extensionPointsAvailable]
+(
+ @accountID INT
+)
+RETURNS int
+AS
+BEGIN
+
+DECLARE @dailySum INT, @penaltySum INT, @ingameSpent INT, @resultEp INT;
+
+SELECT @dailySum= SUM(points) FROM extensionpoints WHERE accountid=@accountID
+select @penaltySum= sum(points) from extensionpointpenalty where accountid=@accountID
+SELECT @ingameSpent= SUM(points) FROM accountextensionspent WHERE accountID=@accountID
+
+SET @dailySum = COALESCE(@dailySum,0);
+SET @penaltySum = COALESCE(@penaltySum,0);
+SET @ingameSpent = COALESCE(@ingameSpent,0);
+
+SET @resultEp = @dailySum - @penaltySum - @ingameSpent;
+RETURN @resultEp;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/extensionPointsCollected.sql b/docs/db_structure/functions/extensionPointsCollected.sql
new file mode 100644
index 0000000..f00db80
--- /dev/null
+++ b/docs/db_structure/functions/extensionPointsCollected.sql
@@ -0,0 +1,29 @@
+/****** Object: UserDefinedFunction [dbo].[extensionPointsCollected] Script Date: 10.05.2026 10:28:10 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[extensionPointsCollected]
+(
+ @accountID INT
+)
+RETURNS int
+AS
+BEGIN
+
+DECLARE @dailySum INT, @penaltySum INT, @resultEp INT;
+
+SELECT @dailySum= SUM(points) FROM extensionpoints WHERE accountid=@accountID
+select @penaltySum= sum(points) from extensionpointpenalty where accountid=@accountID
+
+SET @dailySum = COALESCE(@dailySum,0);
+SET @penaltySum = COALESCE(@penaltySum,0);
+
+SET @resultEp = @dailySum - @penaltySum ;
+RETURN @resultEp;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/extensionSubscriptionIsAcitve.sql b/docs/db_structure/functions/extensionSubscriptionIsAcitve.sql
new file mode 100644
index 0000000..d8d240d
--- /dev/null
+++ b/docs/db_structure/functions/extensionSubscriptionIsAcitve.sql
@@ -0,0 +1,25 @@
+/****** Object: UserDefinedFunction [dbo].[extensionSubscriptionIsAcitve] Script Date: 10.05.2026 10:29:02 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[extensionSubscriptionIsAcitve]
+(
+ @accountID INT,
+ @questionedTime DATETIME
+)
+RETURNS bit
+AS
+BEGIN
+ IF EXISTS (SELECT * FROM extensionsubscription WHERE accountid=@accountID AND starttime < @questionedTime AND endtime > @questionedTime )
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/extremeDefsByCfField.sql b/docs/db_structure/functions/extremeDefsByCfField.sql
new file mode 100644
index 0000000..55cd884
--- /dev/null
+++ b/docs/db_structure/functions/extremeDefsByCfField.sql
@@ -0,0 +1,39 @@
+/****** Object: UserDefinedFunction [dbo].[extremeDefsByCfField] Script Date: 10.05.2026 10:30:39 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[extremeDefsByCfField]
+(
+ @definitions IntList READONLY,
+ @fieldId INT,
+ @returnMax BIT
+)
+RETURNS VARCHAR(100)
+AS
+BEGIN
+DECLARE @maxName VARCHAR(100),@minName VARCHAR(100);
+
+SELECT TOP 1 @maxName=dbo.GetDefinitionName(ar.definition)
+FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId ORDER BY ar.value DESC;
+
+SELECT TOP 1 @minName=dbo.GetDefinitionName(ar.definition)
+FROM dbo.aggregateRecordsByIds(@definitions) ar WHERE ar.field=@fieldId ORDER BY ar.value asc;
+
+
+IF (@returnMax=1)
+BEGIN
+ RETURN @maxName;
+END
+
+RETURN @minName;
+
+
+END
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/fn_CalculateDynamicPlasmaPrices.sql b/docs/db_structure/functions/fn_CalculateDynamicPlasmaPrices.sql
new file mode 100644
index 0000000..8bea107
--- /dev/null
+++ b/docs/db_structure/functions/fn_CalculateDynamicPlasmaPrices.sql
@@ -0,0 +1,80 @@
+/****** Object: UserDefinedFunction [dbo].[fn_CalculateDynamicPlasmaPrices] Script Date: 10.05.2026 9:53:13 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+---- Create table function to calculate dynamic prices for a given island type
+
+CREATE FUNCTION [dbo].[fn_CalculateDynamicPlasmaPrices] (@island_type INT)
+RETURNS TABLE
+AS
+RETURN
+(
+ WITH parameters AS (
+ SELECT
+ MIN_PRICE =
+ CASE @island_type
+ WHEN 1 THEN 25
+ WHEN 2 THEN 125
+ WHEN 3 THEN 200
+ ELSE 0
+ END,
+ MAX_PRICE =
+ CASE @island_type
+ WHEN 1 THEN 150
+ WHEN 2 THEN 250
+ WHEN 3 THEN 275
+ ELSE 0
+ END
+ ),
+ weights AS (
+ SELECT * FROM (VALUES
+ ('def_common_reactor_plasma', 1.0),
+ ('def_thelodica_reactor_plasma', 1.0),
+ ('def_pelistal_reactor_plasma', 1.0),
+ ('def_nuimqol_reactor_plasma', 1.0)
+ ) AS w(plasma_type, weight)
+ ),
+ gathered_cte AS (
+ SELECT plasma_type, SUM(quantity) AS gathered
+ FROM plasma_gathered
+ WHERE gathered_on >= DATEADD(DAY, -7, CAST(GETDATE() AS DATE))
+ GROUP BY plasma_type
+ ),
+ sold_cte AS (
+ SELECT plasma_type, SUM(quantity) AS sold
+ FROM plasma_sold
+ WHERE sold_on >= DATEADD(DAY, -7, CAST(GETDATE() AS DATE))
+ GROUP BY plasma_type
+ )
+ SELECT
+ w.plasma_type,
+ g.gathered,
+ s.sold,
+ w.weight,
+ CAST(
+ (
+ CASE
+ WHEN ISNULL(g.gathered, 0) = 0 THEN p.MAX_PRICE
+ ELSE
+ p.MIN_PRICE + (p.MAX_PRICE - p.MIN_PRICE) *
+ (
+ CASE
+ WHEN CAST(ISNULL(s.sold, 0) AS FLOAT) / NULLIF(g.gathered, 1) > 1 THEN 0
+ ELSE 1.0 - (CAST(ISNULL(s.sold, 0) AS FLOAT) / NULLIF(g.gathered, 1))
+ END
+ )
+ END
+ ) * w.weight AS DECIMAL(10, 2)
+ ) AS dynamic_price
+ FROM weights w
+ CROSS JOIN parameters p
+ LEFT JOIN gathered_cte g ON g.plasma_type = w.plasma_type
+ LEFT JOIN sold_cte s ON s.plasma_type = w.plasma_type
+);
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getAggregateName.sql b/docs/db_structure/functions/getAggregateName.sql
new file mode 100644
index 0000000..7ca2695
--- /dev/null
+++ b/docs/db_structure/functions/getAggregateName.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[getAggregateName] Script Date: 10.05.2026 10:31:20 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getAggregateName]
+(
+ @fieldID int
+)
+RETURNS VARCHAR(100)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(100)
+
+ SELECT @Result=name FROM dbo.aggregatefields WHERE id=@fieldID
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getAllianceEid.sql b/docs/db_structure/functions/getAllianceEid.sql
new file mode 100644
index 0000000..09f17ff
--- /dev/null
+++ b/docs/db_structure/functions/getAllianceEid.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[getAllianceEid] Script Date: 10.05.2026 10:31:56 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[getAllianceEid]
+(
+ @characterId int
+)
+RETURNS bigint
+AS
+BEGIN
+
+ DECLARE @Result bigint, @corpEid bigint
+ SELECT @corpEid = (select corporationeid from corporationmembers where memberid=@characterId)
+ SELECT @Result = (select allianceeid from alliancemembers where corporationeid=@corpEid)
+ RETURN @Result
+
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getBaseEIDByCharacterID.sql b/docs/db_structure/functions/getBaseEIDByCharacterID.sql
new file mode 100644
index 0000000..56f7810
--- /dev/null
+++ b/docs/db_structure/functions/getBaseEIDByCharacterID.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[getBaseEIDByCharacterID] Script Date: 10.05.2026 10:32:38 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+CREATE FUNCTION [dbo].[getBaseEIDByCharacterID]
+(
+ -- Add the parameters for the function here
+ @characterID int
+)
+RETURNS bigint
+AS
+BEGIN
+ declare @result as bigint
+
+ select @result = (SELECT baseEID from characters where characterid = @characterID)
+
+ RETURN @result
+
+END
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getBetaArtifactIdFromAlphaId.sql b/docs/db_structure/functions/getBetaArtifactIdFromAlphaId.sql
new file mode 100644
index 0000000..1c535e7
--- /dev/null
+++ b/docs/db_structure/functions/getBetaArtifactIdFromAlphaId.sql
@@ -0,0 +1,42 @@
+/****** Object: UserDefinedFunction [dbo].[getBetaArtifactIdFromAlphaId] Script Date: 10.05.2026 10:33:08 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getBetaArtifactIdFromAlphaId]
+(
+
+ @artifactType int
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result INT, @newName VARCHAR(50), @currentName VARCHAR(50)
+
+ SET @currentName = (SELECT [name] FROM artifacttypes WHERE id=@artifactType)
+
+ IF @currentName IS NULL
+ BEGIN
+ RETURN -2
+ end
+
+
+ SET @newName = REPLACE(@currentName,'_alpha','_beta')
+
+ SET @Result = (SELECT id FROM artifacttypes WHERE [name]=@newName)
+
+ IF @Result IS NULL
+ BEGIN
+ RETURN -1
+ END
+
+
+ -- Return the result of the function
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getCalendar.sql b/docs/db_structure/functions/getCalendar.sql
new file mode 100644
index 0000000..8b57baf
--- /dev/null
+++ b/docs/db_structure/functions/getCalendar.sql
@@ -0,0 +1,62 @@
+/****** Object: UserDefinedFunction [dbo].[getCalendar] Script Date: 10.05.2026 9:53:50 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[getCalendar]
+(
+ @days int
+)
+returns @t2 table (dt datetime)
+
+AS
+begin
+
+declare @startDate as smalldatetime
+
+set @startDate = convert(smalldatetime,convert(char(8),dateadd(day,-(@days - 1),getdate()),112))
+
+insert into @t2
+select top(@days) *
+from
+(
+select @startDate + n3.num * 100 + n2.num * 10 +n1.num as n
+from
+(
+ select 0 as num union all
+ select 1 union all
+ select 2 union all
+ select 3 union all
+ select 4 union all
+ select 5 union all
+ select 6 union all
+ select 7 union all
+ select 8 union all
+ select 9
+) n1,
+(
+ select 0 as num union all
+ select 1 union all
+ select 2 union all
+ select 3 union all
+ select 4 union all
+ select 5 union all
+ select 6 union all
+ select 7 union all
+ select 8 union all
+ select 9
+) n2,
+(
+ select 0 as num union all
+ select 1 union all
+ select 2 union all
+ select 3
+) n3
+) gencalendar
+order by 1
+
+return
+end
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getCharacterEID.sql b/docs/db_structure/functions/getCharacterEID.sql
new file mode 100644
index 0000000..df7719e
--- /dev/null
+++ b/docs/db_structure/functions/getCharacterEID.sql
@@ -0,0 +1,28 @@
+/****** Object: UserDefinedFunction [dbo].[getCharacterEID] Script Date: 10.05.2026 10:34:46 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+-- =============================================
+-- Author:
+-- Create date:
+-- Description:
+-- =============================================
+CREATE FUNCTION [dbo].[getCharacterEID]
+(
+ @characterid int
+)
+RETURNS bigint
+AS
+BEGIN
+ declare @result as bigint
+
+ select @result = rootEID from characters where characterid = @characterid
+
+ return @result
+END
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getCorporationEid.sql b/docs/db_structure/functions/getCorporationEid.sql
new file mode 100644
index 0000000..8d84730
--- /dev/null
+++ b/docs/db_structure/functions/getCorporationEid.sql
@@ -0,0 +1,22 @@
+/****** Object: UserDefinedFunction [dbo].[getCorporationEid] Script Date: 10.05.2026 10:35:24 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getCorporationEid]
+(
+ @characterId int
+)
+RETURNS bigint
+AS
+BEGIN
+
+ DECLARE @Result bigint
+ SELECT @Result = (select corporationeid from corporationmembers where memberid=@characterId)
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getCorporationEidAndRole.sql b/docs/db_structure/functions/getCorporationEidAndRole.sql
new file mode 100644
index 0000000..156e663
--- /dev/null
+++ b/docs/db_structure/functions/getCorporationEidAndRole.sql
@@ -0,0 +1,19 @@
+/****** Object: UserDefinedFunction [dbo].[getCorporationEidAndRole] Script Date: 10.05.2026 9:54:31 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getCorporationEidAndRole]
+(
+ @characterId int
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT corporationEID,[role] FROM dbo.corporationmembers WHERE memberid=@characterId
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getCorporationNameByCharacterEID.sql b/docs/db_structure/functions/getCorporationNameByCharacterEID.sql
new file mode 100644
index 0000000..1220aa4
--- /dev/null
+++ b/docs/db_structure/functions/getCorporationNameByCharacterEID.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[getCorporationNameByCharacterEID] Script Date: 10.05.2026 10:36:31 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[getCorporationNameByCharacterEID]
+(
+
+ @characterEID bigint
+)
+RETURNS VARCHAR(128)
+AS
+BEGIN
+
+ DECLARE @corpEid BIGINT, @result VARCHAR(128), @characterID int
+
+ SET @characterID = (SELECT characterID FROM characters WHERE rootEID=@characterEID)
+
+ SET @corpEid = (SELECT corporationeid FROM dbo.corporationmembers WHERE memberid=@characterID)
+
+ SET @result = (SELECT NAME FROM corporations WHERE eid=@corpEid)
+
+ RETURN @result
+
+
+ END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getCorporationNameByCharacterID.sql b/docs/db_structure/functions/getCorporationNameByCharacterID.sql
new file mode 100644
index 0000000..81bcf01
--- /dev/null
+++ b/docs/db_structure/functions/getCorporationNameByCharacterID.sql
@@ -0,0 +1,29 @@
+/****** Object: UserDefinedFunction [dbo].[getCorporationNameByCharacterID] Script Date: 10.05.2026 10:37:07 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getCorporationNameByCharacterID]
+(
+
+ @characterID int
+)
+RETURNS VARCHAR(128)
+AS
+BEGIN
+
+ DECLARE @corpEid BIGINT, @result VARCHAR(128)
+
+
+ SET @corpEid = (SELECT corporationeid FROM dbo.corporationmembers WHERE memberid=@characterID)
+
+ SET @result = (SELECT NAME FROM corporations WHERE eid=@corpEid)
+
+ RETURN @result
+
+
+ END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDefaultDockingBasePositions.sql b/docs/db_structure/functions/getDefaultDockingBasePositions.sql
new file mode 100644
index 0000000..b4e2ca9
--- /dev/null
+++ b/docs/db_structure/functions/getDefaultDockingBasePositions.sql
@@ -0,0 +1,31 @@
+/****** Object: UserDefinedFunction [dbo].[getDefaultDockingBasePositions] Script Date: 10.05.2026 9:55:14 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getDefaultDockingBasePositions]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+SELECT ze.eid,ze.zoneID,ze.x,ze.y,ze.z ,z.x AS zonex,z.y AS zoney
+FROM dbo.zoneentities ze
+JOIN dbo.entities e ON ze.eid = e.eid
+JOIN dbo.zones z ON z.id = ze.zoneID
+WHERE e.definition IN
+(
+SELECT definition FROM dbo.getDefinitionByCFString('cf_public_docking_base')
+)
+AND
+z.zonetype !=3
+
+
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDefinitionByCF.sql b/docs/db_structure/functions/getDefinitionByCF.sql
new file mode 100644
index 0000000..14334e5
--- /dev/null
+++ b/docs/db_structure/functions/getDefinitionByCF.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[getDefinitionByCF] Script Date: 10.05.2026 9:56:24 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+create FUNCTION [dbo].[getDefinitionByCF]
+(
+ @categoryflag bigint
+
+)
+returns @t2 table (definition int)
+
+AS
+begin
+DECLARE @mask BIGINT
+SET @mask = dbo.GetCFMask(@categoryflag)
+insert into @t2
+SELECT definition FROM dbo.entitydefaults WHERE (categoryflags & @mask) = @categoryflag
+
+return
+end
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDefinitionByCFString.sql b/docs/db_structure/functions/getDefinitionByCFString.sql
new file mode 100644
index 0000000..a468b16
--- /dev/null
+++ b/docs/db_structure/functions/getDefinitionByCFString.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[getDefinitionByCFString] Script Date: 10.05.2026 9:56:57 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+create FUNCTION [dbo].[getDefinitionByCFString]
+(
+ @cfString VARCHAR(128)
+
+)
+returns @t2 table (definition int)
+
+AS
+begin
+DECLARE @mask BIGINT, @categoryflag BIGINT
+
+SET @categoryflag = (SELECT [value] FROM dbo.categoryFlags WHERE name=@cfString)
+
+SET @mask = dbo.GetCFMask(@categoryflag)
+insert into @t2
+SELECT definition FROM dbo.entitydefaults WHERE (categoryflags & @mask) = @categoryflag
+
+return
+end
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDefinitionByCategoryflag.sql b/docs/db_structure/functions/getDefinitionByCategoryflag.sql
new file mode 100644
index 0000000..fb19521
--- /dev/null
+++ b/docs/db_structure/functions/getDefinitionByCategoryflag.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[getDefinitionByCategoryflag] Script Date: 10.05.2026 9:55:49 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[getDefinitionByCategoryflag]
+(
+ @categoryflag bigint,
+ @mask bigint
+)
+returns @t2 table (definition int)
+
+AS
+begin
+
+insert into @t2
+
+SELECT definition FROM dbo.entitydefaults WHERE (categoryflags & @mask) = @categoryflag
+
+return
+end
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDefinitionByEID.sql b/docs/db_structure/functions/getDefinitionByEID.sql
new file mode 100644
index 0000000..db63432
--- /dev/null
+++ b/docs/db_structure/functions/getDefinitionByEID.sql
@@ -0,0 +1,22 @@
+/****** Object: UserDefinedFunction [dbo].[getDefinitionByEID] Script Date: 10.05.2026 10:38:40 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[getDefinitionByEID]
+(
+ @eid bigint
+)
+returns int
+AS
+BEGIN
+ declare @result as int
+
+ select @result = definition from entities where eid = @eid
+
+ return @result
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDockingbaseChildrenFromActiveZones.sql b/docs/db_structure/functions/getDockingbaseChildrenFromActiveZones.sql
new file mode 100644
index 0000000..4e69a89
--- /dev/null
+++ b/docs/db_structure/functions/getDockingbaseChildrenFromActiveZones.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[getDockingbaseChildrenFromActiveZones] Script Date: 10.05.2026 9:57:32 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+Create FUNCTION [dbo].[getDockingbaseChildrenFromActiveZones] ()
+RETURNS TABLE
+AS
+RETURN
+(
+
+WITH baseEids (eid) as
+(
+SELECT e.eid FROM dbo.entities e
+JOIN dbo.zoneentities ze ON e.eid=ze.eid
+JOIN dbo.zones z1 ON ze.zoneID=z1.id
+WHERE e.definition IN (SELECT [definition] FROM dbo.getDockingbaseDefinitions()) AND z1.enabled=1
+UNION
+SELECT e.eid FROM dbo.entities e
+JOIN dbo.zoneuserentities zue ON e.eid = zue.eid
+JOIN dbo.zones z2 ON zue.zoneid=z2.id
+WHERE e.definition IN (SELECT [definition] FROM dbo.getDockingbaseDefinitions()) AND z2.enabled=1
+)
+SELECT * FROM dbo.entities WHERE parent IN (SELECT eid FROM baseEids)
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getDockingbaseDefinitions.sql b/docs/db_structure/functions/getDockingbaseDefinitions.sql
new file mode 100644
index 0000000..3a44bff
--- /dev/null
+++ b/docs/db_structure/functions/getDockingbaseDefinitions.sql
@@ -0,0 +1,19 @@
+/****** Object: UserDefinedFunction [dbo].[getDockingbaseDefinitions] Script Date: 10.05.2026 9:58:11 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+CREATE FUNCTION [dbo].[getDockingbaseDefinitions] ()
+RETURNS TABLE
+AS
+RETURN
+(
+SELECT [definition] FROM dbo.getDefinitionByCF(65912)
+union
+SELECT [definition] FROM dbo.getDefinitionByCF(151192722)
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getEntitiesFromZoneByCf.sql b/docs/db_structure/functions/getEntitiesFromZoneByCf.sql
new file mode 100644
index 0000000..adc75e3
--- /dev/null
+++ b/docs/db_structure/functions/getEntitiesFromZoneByCf.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[getEntitiesFromZoneByCf] Script Date: 10.05.2026 9:58:56 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getEntitiesFromZoneByCf]
+(
+ @zoneId INT,
+ @cfString VARCHAR(128)
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+WITH columndefs (definition) AS
+(
+SELECT [definition] FROM dbo.getDefinitionByCFString(@cfString)
+)
+, livezoneeids (eid) AS
+(
+SELECT eid FROM dbo.zoneentities WHERE runtime=0 AND zoneID=@zoneId AND [enabled]=1
+)
+SELECT * FROM dbo.entities e WHERE e.eid IN (SELECT eid FROM livezoneeids) AND e.definition IN (SELECT definition FROM columndefs)
+)
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getIndexFragmentationData.sql b/docs/db_structure/functions/getIndexFragmentationData.sql
new file mode 100644
index 0000000..5708979
--- /dev/null
+++ b/docs/db_structure/functions/getIndexFragmentationData.sql
@@ -0,0 +1,34 @@
+/****** Object: UserDefinedFunction [dbo].[getIndexFragmentationData] Script Date: 10.05.2026 9:59:36 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getIndexFragmentationData]
+(
+ @threshold int
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+
+SELECT
+ b.name,
+ a.avg_fragmentation_in_percent as fp,
+ OBJECT_NAME(b.object_id) AS tablename,
+ 'ALTER INDEX '+ cast(b.name as varchar(50))+' ON ' + OBJECT_NAME(b.object_id) + ' REBUILD '
+ AS todo
+
+FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS a
+ JOIN sys.indexes AS b ON a.object_id = b.object_id AND a.index_id = b.index_id
+ where
+ a.avg_fragmentation_in_percent > @threshold
+ and
+ b.name is not null
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getInnerBetaZoneIdFromAlpha.sql b/docs/db_structure/functions/getInnerBetaZoneIdFromAlpha.sql
new file mode 100644
index 0000000..2d0252e
--- /dev/null
+++ b/docs/db_structure/functions/getInnerBetaZoneIdFromAlpha.sql
@@ -0,0 +1,31 @@
+/****** Object: UserDefinedFunction [dbo].[getInnerBetaZoneIdFromAlpha] Script Date: 10.05.2026 10:39:50 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getInnerBetaZoneIdFromAlpha]
+(
+
+ @zoneId int
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result int
+
+ SET @Result =
+ CASE
+ WHEN @zoneId=0 THEN 5
+ WHEN @zoneId=1 THEN 3
+ WHEN @zoneId=2 THEN 4
+ end
+
+ -- Return the result of the function
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getIpStatus.sql b/docs/db_structure/functions/getIpStatus.sql
new file mode 100644
index 0000000..9294a7f
--- /dev/null
+++ b/docs/db_structure/functions/getIpStatus.sql
@@ -0,0 +1,53 @@
+/****** Object: UserDefinedFunction [dbo].[getIpStatus] Script Date: 10.05.2026 10:40:25 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getIpStatus]
+(
+
+ @value int
+)
+RETURNS varchar(50)
+AS
+BEGIN
+
+ -- Declare the return variable here
+ DECLARE @Result varchar(50)
+
+ set @Result = (select 'tofi' =
+ case
+ when @value = -1 then 'Unknown'
+ when @value = 0 then 'Success'
+ when @value = 11002 then 'DestinationNetworkUnreachable'
+ when @value = 11003 then 'DestinationHostUnreachable'
+ when @value = 11004 then 'DestinationProtocolUnreachable'
+ when @value = 11005 then 'DestinationPortUnreachable'
+ when @value = 11006 then 'NoResources'
+ when @value = 11007 then 'BadOption'
+ when @value = 11008 then 'HardwareError'
+ when @value = 11009 then 'PacketTooBig'
+ when @value = 11010 then 'TimedOut'
+ when @value = 11012 then 'BadRoute'
+ when @value = 11013 then 'TtlExpired'
+ when @value = 11014 then 'TtlReassemblyTimeExceeded'
+ when @value = 11015 then 'ParameterProblem'
+ when @value = 11016 then 'SourceQuench'
+ when @value = 11018 then 'BadDestination'
+ when @value = 11040 then 'DestinationUnreachable'
+ when @value = 11041 then 'TimeExceeded'
+ when @value = 11042 then 'BadHeader'
+ when @value = 11043 then 'UnrecognizedNextHeader'
+ when @value = 11044 then 'IcmpError'
+ when @value = 11045 then 'DestinationScopeMismatch'
+ else 'wtf'
+ end)
+
+ -- Return the result of the function
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveBetaMarkets.sql b/docs/db_structure/functions/getLiveBetaMarkets.sql
new file mode 100644
index 0000000..e1987e9
--- /dev/null
+++ b/docs/db_structure/functions/getLiveBetaMarkets.sql
@@ -0,0 +1,26 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveBetaMarkets] Script Date: 10.05.2026 10:00:19 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[getLiveBetaMarkets]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT eid FROM dbo.entities WHERE definition=10 and parent IN
+ (
+ SELECT e.eid FROM dbo.zoneentities ze
+ JOIN dbo.entities e ON e.eid=ze.eid
+ JOIN dbo.zones z ON ze.zoneID = z.id
+ WHERE e.definition IN(SELECT [definition] FROM dbo.getDefinitionByCFString('cf_public_docking_base'))
+ AND z.terraformable=0 AND z.protected=0
+ )
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveDefaultMarkets.sql b/docs/db_structure/functions/getLiveDefaultMarkets.sql
new file mode 100644
index 0000000..c45c4db
--- /dev/null
+++ b/docs/db_structure/functions/getLiveDefaultMarkets.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveDefaultMarkets] Script Date: 10.05.2026 10:01:09 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[getLiveDefaultMarkets]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT eid FROM dbo.entities WHERE definition=10 and parent IN
+ (
+ SELECT e.eid FROM dbo.zoneentities ze
+ JOIN dbo.entities e ON e.eid=ze.eid
+ WHERE e.definition IN(SELECT [definition] FROM dbo.getDefinitionByCFString('cf_public_docking_base'))
+ )
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveDockingbaseChildren.sql b/docs/db_structure/functions/getLiveDockingbaseChildren.sql
new file mode 100644
index 0000000..82cb461
--- /dev/null
+++ b/docs/db_structure/functions/getLiveDockingbaseChildren.sql
@@ -0,0 +1,28 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveDockingbaseChildren] Script Date: 10.05.2026 10:01:51 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getLiveDockingbaseChildren] ()
+RETURNS TABLE
+AS
+RETURN
+(
+
+WITH baseEids (eid) as
+(
+SELECT e.eid FROM dbo.entities e
+JOIN dbo.zoneentities ze ON e.eid=ze.eid
+WHERE e.definition IN (SELECT [definition] FROM dbo.getDockingbaseDefinitions())
+UNION
+SELECT e.eid FROM dbo.entities e
+JOIN dbo.zoneuserentities zue ON e.eid = zue.eid
+WHERE e.definition IN (SELECT [definition] FROM dbo.getDockingbaseDefinitions())
+)
+SELECT * FROM dbo.entities WHERE parent IN (SELECT eid FROM baseEids)
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveFieldTerminalChildren.sql b/docs/db_structure/functions/getLiveFieldTerminalChildren.sql
new file mode 100644
index 0000000..592577c
--- /dev/null
+++ b/docs/db_structure/functions/getLiveFieldTerminalChildren.sql
@@ -0,0 +1,28 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveFieldTerminalChildren] Script Date: 10.05.2026 10:02:26 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getLiveFieldTerminalChildren]
+(
+ @zoneID int
+)
+
+RETURNS TABLE
+AS
+RETURN
+(
+ WITH ftEids (eid) as
+ (
+ SELECT e.eid FROM dbo.entities e
+ JOIN dbo.zoneentities ze ON e.eid=ze.eid
+ WHERE e.definition IN (SELECT definition FROM dbo.getDefinitionByCF(131448)) AND
+ @zoneID = ze.zoneID
+ )
+ SELECT * FROM dbo.entities WHERE parent IN (SELECT eid FROM ftEids)
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveGammaDockingBases.sql b/docs/db_structure/functions/getLiveGammaDockingBases.sql
new file mode 100644
index 0000000..c03724f
--- /dev/null
+++ b/docs/db_structure/functions/getLiveGammaDockingBases.sql
@@ -0,0 +1,21 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveGammaDockingBases] Script Date: 10.05.2026 10:03:04 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getLiveGammaDockingBases]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+SELECT e.* FROM dbo.zoneuserentities ze
+JOIN dbo.entities e ON e.eid=ze.eid
+WHERE e.definition IN(SELECT [definition] FROM dbo.getDefinitionByCFString('cf_pbs_docking_base'))
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveGammaMarkets.sql b/docs/db_structure/functions/getLiveGammaMarkets.sql
new file mode 100644
index 0000000..a75cc44
--- /dev/null
+++ b/docs/db_structure/functions/getLiveGammaMarkets.sql
@@ -0,0 +1,20 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveGammaMarkets] Script Date: 10.05.2026 10:03:40 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getLiveGammaMarkets]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT * FROM dbo.entities WHERE definition=10 and parent IN
+ (SELECT eid FROM dbo.getLiveGammaDockingBases())
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getLiveStructureChildren.sql b/docs/db_structure/functions/getLiveStructureChildren.sql
new file mode 100644
index 0000000..1637308
--- /dev/null
+++ b/docs/db_structure/functions/getLiveStructureChildren.sql
@@ -0,0 +1,28 @@
+/****** Object: UserDefinedFunction [dbo].[getLiveStructureChildren] Script Date: 10.05.2026 10:04:18 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[getLiveStructureChildren]
+(
+ @zoneID int
+)
+
+RETURNS TABLE
+AS
+RETURN
+(
+ WITH ftEids (eid) as
+ (
+ SELECT e.eid FROM dbo.entities e
+ JOIN dbo.zoneentities ze ON e.eid=ze.eid
+ WHERE e.definition IN (SELECT definition FROM dbo.getDefinitionByCF(0x178)) AND
+ @zoneID = ze.zoneID
+ )
+ SELECT * FROM dbo.entities WHERE parent IN (SELECT eid FROM ftEids)
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getMissionCategoryName.sql b/docs/db_structure/functions/getMissionCategoryName.sql
new file mode 100644
index 0000000..5baa601
--- /dev/null
+++ b/docs/db_structure/functions/getMissionCategoryName.sql
@@ -0,0 +1,22 @@
+/****** Object: UserDefinedFunction [dbo].[getMissionCategoryName] Script Date: 10.05.2026 10:40:56 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getMissionCategoryName]
+(
+ @categoryValue int
+)
+RETURNS VARCHAR(64)
+AS
+BEGIN
+
+ DECLARE @Result VARCHAR(64)
+ SELECT @Result = (SELECT TOP 1 category FROM dbo.missiontypes WHERE categoryvalue=@categoryValue)
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getNickByEid.sql b/docs/db_structure/functions/getNickByEid.sql
new file mode 100644
index 0000000..f07e77e
--- /dev/null
+++ b/docs/db_structure/functions/getNickByEid.sql
@@ -0,0 +1,22 @@
+/****** Object: UserDefinedFunction [dbo].[getNickByEid] Script Date: 10.05.2026 10:42:07 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+create FUNCTION [dbo].[getNickByEid]
+(
+ @eid bigint
+)
+returns varchar(64)
+AS
+BEGIN
+ declare @result as varchar(64)
+
+ select @result = (select nick from characters where rooteid=@eid)
+
+ return @result
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getPBSDefinitionFromCapsule.sql b/docs/db_structure/functions/getPBSDefinitionFromCapsule.sql
new file mode 100644
index 0000000..b8e1e73
--- /dev/null
+++ b/docs/db_structure/functions/getPBSDefinitionFromCapsule.sql
@@ -0,0 +1,35 @@
+/****** Object: UserDefinedFunction [dbo].[getPBSDefinitionFromCapsule] Script Date: 10.05.2026 10:42:40 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getPBSDefinitionFromCapsule]
+(
+ @capsuleDefinition int
+)
+RETURNS int
+AS
+BEGIN
+ -- Declare the return variable here
+ DECLARE @objectDefinition INT, @pbsDefinition INT
+
+ SELECT @objectDefinition=targetdefinition FROM dbo.definitionconfig WHERE definition=@capsuleDefinition
+
+ IF (@objectDefinition IS NULL)
+ BEGIN
+ RETURN 0;
+ END
+
+ SELECT @pbsDefinition=targetdefinition FROM dbo.definitionconfig WHERE definition=@objectDefinition
+
+ IF (@objectDefinition IS NULL)
+ BEGIN
+ RETURN 0;
+ END
+
+ RETURN @pbsDefinition
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getTree.sql b/docs/db_structure/functions/getTree.sql
new file mode 100644
index 0000000..9d3ed0e
--- /dev/null
+++ b/docs/db_structure/functions/getTree.sql
@@ -0,0 +1,32 @@
+/****** Object: UserDefinedFunction [dbo].[getTree] Script Date: 10.05.2026 10:05:44 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getTree]
+(
+ @rootEID BIGINT
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+with children(eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, lvl)
+ as
+ (
+ SELECT eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, 0 FROM entities WHERE eid = @rootEID
+
+ UNION ALL
+
+ SELECT C.eid,C.definition,C.owner,C.parent,C.repackaged,C.quantity,C.health,C.ename,c.dynprop, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid
+ )
+
+select eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop,lvl from children
+
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getTreeByRoot.sql b/docs/db_structure/functions/getTreeByRoot.sql
new file mode 100644
index 0000000..764a9f0
--- /dev/null
+++ b/docs/db_structure/functions/getTreeByRoot.sql
@@ -0,0 +1,32 @@
+/****** Object: UserDefinedFunction [dbo].[getTreeByRoot] Script Date: 10.05.2026 10:06:21 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getTreeByRoot]
+(
+ @rootEID bigint -- the node which will be used as root
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+
+with children(eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, lvl)
+ as
+ (
+ SELECT eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, 0 FROM entities WHERE eid = @rootEID
+
+ UNION ALL
+
+ SELECT C.eid,C.definition,C.owner,C.parent,C.repackaged,C.quantity,C.health,C.ename,c.dynprop, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid
+ )
+
+select eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop,lvl from children
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getVendorMarketPrice.sql b/docs/db_structure/functions/getVendorMarketPrice.sql
new file mode 100644
index 0000000..78631f1
--- /dev/null
+++ b/docs/db_structure/functions/getVendorMarketPrice.sql
@@ -0,0 +1,48 @@
+/****** Object: UserDefinedFunction [dbo].[getVendorMarketPrice] Script Date: 10.05.2026 10:45:18 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[getVendorMarketPrice]
+(
+ @vendorEID bigint,
+ @definition INT,
+ @isSell BIT
+
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @manualPrice bit
+ declare @price bigint
+ declare @vendorProfit float
+ declare @quantity int
+
+ SET @manualPrice = (SELECT manualprice FROM itemprices WHERE definition =@definition)
+ set @quantity = (select quantity from entitydefaults where definition=@definition)
+
+ set @price = dbo.calcPrice(@definition) / @quantity -- get price from itemprices
+
+ IF (@isSell=1)
+ begin
+ set @vendorProfit = (select vendorsellprofit from vendors where vendorEID=@vendorEID)
+ END
+ ELSE
+ BEGIN
+ SET @vendorProfit = (SELECT vendorbuyprofit FROM dbo.vendors where vendorEID=@vendorEID)
+ END
+
+ IF (@manualPrice=1)
+ BEGIN
+ SET @vendorProfit = 1
+ end
+
+
+ RETURN @price * @vendorProfit
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getVendorSellPrice.sql b/docs/db_structure/functions/getVendorSellPrice.sql
new file mode 100644
index 0000000..a50af3a
--- /dev/null
+++ b/docs/db_structure/functions/getVendorSellPrice.sql
@@ -0,0 +1,34 @@
+/****** Object: UserDefinedFunction [dbo].[getVendorSellPrice] Script Date: 10.05.2026 10:45:58 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+CREATE FUNCTION [dbo].[getVendorSellPrice]
+(
+ @vendorEID bigint,
+ @definition int
+)
+RETURNS int
+AS
+BEGIN
+
+ declare @price bigint
+ declare @vendorsellprofit float
+ declare @quantity int
+
+ set @quantity = (select quantity from entitydefaults where definition=@definition)
+
+
+
+ set @price = dbo.calcPrice(@definition) / @quantity -- get price from itemprices
+ set @vendorsellprofit = (select vendorsellprofit from vendors where vendorEID=@vendorEID)
+
+ RETURN @price * @vendorsellprofit
+
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/getdaystring.sql b/docs/db_structure/functions/getdaystring.sql
new file mode 100644
index 0000000..90b022a
--- /dev/null
+++ b/docs/db_structure/functions/getdaystring.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[getdaystring] Script Date: 10.05.2026 10:38:09 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE FUNCTION [dbo].[getdaystring]
+(
+
+ @date datetime
+)
+RETURNS varchar(20)
+AS
+BEGIN
+
+ DECLARE @ResultVar varchar(20),@year INT,@month INT, @day INT
+
+
+ SET @year =DATEPART(year,@date);
+
+ SET @month =datepart(month,@date);
+ SET @day =DATEPART(day,@date);
+
+ set @ResultVar = CAST( @year as varchar(4)) + '-' + dbo.padLeft(@month,'0',2) + '-' + dbo.padLeft(@day,'0',2);
+
+ RETURN @ResultVar
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/intToInsertString.sql b/docs/db_structure/functions/intToInsertString.sql
new file mode 100644
index 0000000..e69de29
diff --git a/docs/db_structure/functions/invalidBaseChannels.sql b/docs/db_structure/functions/invalidBaseChannels.sql
new file mode 100644
index 0000000..0d544f1
--- /dev/null
+++ b/docs/db_structure/functions/invalidBaseChannels.sql
@@ -0,0 +1,25 @@
+/****** Object: UserDefinedFunction [dbo].[invalidBaseChannels] Script Date: 10.05.2026 10:06:55 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+--nem letezo bazisokhoz channel
+CREATE FUNCTION [dbo].[invalidBaseChannels]
+(
+
+)
+RETURNS TABLE
+AS
+RETURN
+(
+select id from channels where name like 'base_%' and id not in
+(
+select c.id from channels c
+join entities e on c.name = ('base_' + cast(e.eid as varchar(50)))
+where c.name like 'base_%'
+)
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/isAccountTrial.sql b/docs/db_structure/functions/isAccountTrial.sql
new file mode 100644
index 0000000..2e0a0d2
--- /dev/null
+++ b/docs/db_structure/functions/isAccountTrial.sql
@@ -0,0 +1,29 @@
+/****** Object: UserDefinedFunction [dbo].[isAccountTrial] Script Date: 10.05.2026 10:47:34 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[isAccountTrial]
+(
+ @accountID int
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result INT,@acclevel int
+
+ SET @acclevel = (SELECT acclevel FROM accounts WHERE accountID=@accountID)
+
+ IF ((@acclevel & 8388608) > 0)
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/isAggregateFieldMoreIsBetter.sql b/docs/db_structure/functions/isAggregateFieldMoreIsBetter.sql
new file mode 100644
index 0000000..42a9ed2
--- /dev/null
+++ b/docs/db_structure/functions/isAggregateFieldMoreIsBetter.sql
@@ -0,0 +1,34 @@
+/****** Object: UserDefinedFunction [dbo].[isAggregateFieldMoreIsBetter] Script Date: 10.05.2026 10:48:32 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[isAggregateFieldMoreIsBetter]
+(
+ @fieldId INT
+)
+RETURNS VARCHAR(5)
+AS
+BEGIN
+ DECLARE @result VARCHAR(5) = 'n/a';
+
+ DECLARE @moreIsBetter BIT;
+ SELECT @moreIsBetter=moreisbetter FROM dbo.aggregatefields WHERE id=@fieldId;
+
+ IF (@moreIsBetter=1)
+ BEGIN
+ SET @result = 'Yes';
+ END
+
+ IF (@moreIsBetter=0)
+ BEGIN
+ SET @result = 'No';
+ END
+
+ RETURN @result;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/isDefinitionSparkUnlocker.sql b/docs/db_structure/functions/isDefinitionSparkUnlocker.sql
new file mode 100644
index 0000000..c20a7e5
--- /dev/null
+++ b/docs/db_structure/functions/isDefinitionSparkUnlocker.sql
@@ -0,0 +1,31 @@
+/****** Object: UserDefinedFunction [dbo].[isDefinitionSparkUnlocker] Script Date: 10.05.2026 10:50:15 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[isDefinitionSparkUnlocker]
+(
+ @definition int
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @sparkActivatorsCF BIGINT;
+ SET @sparkActivatorsCF = (SELECT value FROM dbo.categoryFlags WHERE name='cf_package_activator_spark');
+
+ DECLARE @defCF BIGINT;
+ SET @defCF = (SELECT d.categoryflags FROM dbo.entitydefaults d WHERE d.[definition]=@definition);
+
+ IF (@defCF = @sparkActivatorsCF)
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/isEntityExists.sql b/docs/db_structure/functions/isEntityExists.sql
new file mode 100644
index 0000000..5b76daf
--- /dev/null
+++ b/docs/db_structure/functions/isEntityExists.sql
@@ -0,0 +1,24 @@
+/****** Object: UserDefinedFunction [dbo].[isEntityExists] Script Date: 10.05.2026 10:50:59 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[isEntityExists]
+(
+ @eid BIGINT
+)
+RETURNS bit
+AS
+BEGIN
+ IF EXISTS (SELECT 1 FROM dbo.entities WHERE eid=@eid)
+ BEGIN
+ RETURN 1;
+ END
+
+ RETURN 0;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/isInTree.sql b/docs/db_structure/functions/isInTree.sql
new file mode 100644
index 0000000..83bfd90
--- /dev/null
+++ b/docs/db_structure/functions/isInTree.sql
@@ -0,0 +1,42 @@
+/****** Object: UserDefinedFunction [dbo].[isInTree] Script Date: 10.05.2026 10:52:06 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+CREATE FUNCTION [dbo].[isInTree]
+(
+ @childEID bigint,
+ @parentEID bigint,
+ @maxDepth int
+)
+RETURNS int
+AS
+BEGIN
+
+ declare @tmpParent bigint
+
+ select @tmpParent = parent from entities where eid=@parentEID
+
+ while (@tmpParent is not NULL)
+ begin
+
+ if (@tmpParent = @childEID or @maxDepth <= 0)
+ begin
+ return 1
+ end
+
+ select @tmpParent = parent from entities where eid=@tmpParent
+ set @maxDepth = @maxDepth - 1
+
+ end
+
+ return 0
+
+
+END
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/isInstance.sql b/docs/db_structure/functions/isInstance.sql
new file mode 100644
index 0000000..559d367
--- /dev/null
+++ b/docs/db_structure/functions/isInstance.sql
@@ -0,0 +1,23 @@
+/****** Object: UserDefinedFunction [dbo].[isInstance] Script Date: 10.05.2026 10:51:33 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[isInstance]
+(
+ @zoneID int
+)
+RETURNS int
+AS
+BEGIN
+ DECLARE @Result int
+
+ SET @Result = (SELECT isInstance FROM dbo.zones WHERE id=@zoneID)
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/listItemsLeftInFieldContainer.sql b/docs/db_structure/functions/listItemsLeftInFieldContainer.sql
new file mode 100644
index 0000000..116a2d3
--- /dev/null
+++ b/docs/db_structure/functions/listItemsLeftInFieldContainer.sql
@@ -0,0 +1,38 @@
+/****** Object: UserDefinedFunction [dbo].[listItemsLeftInFieldContainer] Script Date: 10.05.2026 10:07:31 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[listItemsLeftInFieldContainer]
+(
+ @fieldContainerEid BIGINT
+)
+RETURNS TABLE
+AS
+RETURN
+(
+--47 remove
+--46 add
+
+
+WITH sumlist ([definition],quantity)
+AS
+(
+SELECT [definition], SUM(quantity)* IIF(transactiontype=47,-1,1) FROM charactertransactions
+WHERE containerEID=@fieldContainerEid
+AND transactiontype IN (46,47)
+ and
+definition IS NOT NULL
+and
+quantity IS NOT NULL
+
+GROUP BY definition,transactiontype
+)
+SELECT SUM(quantity) AS quantity ,[definition] AS [definition] FROM sumlist group BY definition HAVING SUM(quantity) > 0
+
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/liveRandomMissions.sql b/docs/db_structure/functions/liveRandomMissions.sql
new file mode 100644
index 0000000..1569215
--- /dev/null
+++ b/docs/db_structure/functions/liveRandomMissions.sql
@@ -0,0 +1,18 @@
+/****** Object: UserDefinedFunction [dbo].[liveRandomMissions] Script Date: 10.05.2026 10:08:07 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[liveRandomMissions]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT * FROM missions WHERE behaviourtype=2 AND title NOT LIKE '%test%' AND listable=1
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/missionLocationsFromZone.sql b/docs/db_structure/functions/missionLocationsFromZone.sql
new file mode 100644
index 0000000..7192c89
--- /dev/null
+++ b/docs/db_structure/functions/missionLocationsFromZone.sql
@@ -0,0 +1,20 @@
+/****** Object: UserDefinedFunction [dbo].[missionLocationsFromZone] Script Date: 10.05.2026 10:08:50 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[missionLocationsFromZone]
+(
+ @zoneId int
+
+)
+RETURNS TABLE
+AS
+RETURN
+(
+ SELECT * FROM dbo.missionlocations WHERE zoneid=@zoneId
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/onlineGangMembers.sql b/docs/db_structure/functions/onlineGangMembers.sql
new file mode 100644
index 0000000..4d239ab
--- /dev/null
+++ b/docs/db_structure/functions/onlineGangMembers.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[onlineGangMembers] Script Date: 10.05.2026 10:09:27 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[onlineGangMembers]
+(
+ @gangGuid UNIQUEIDENTIFIER
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+SELECT gm.memberid
+FROM gangmembers gm
+join characters c ON c.characterID = gm.memberid
+WHERE
+gm.gangid = @gangGuid
+AND
+c.inUse=1
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/padLeft.sql b/docs/db_structure/functions/padLeft.sql
new file mode 100644
index 0000000..e3adef7
--- /dev/null
+++ b/docs/db_structure/functions/padLeft.sql
@@ -0,0 +1,19 @@
+/****** Object: UserDefinedFunction [dbo].[padLeft] Script Date: 10.05.2026 10:52:41 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+Create function [dbo].[padLeft]
+(
+@value varchar(200),
+@padChar char(1)='0',
+@len INT=2
+)
+returns varchar(300)
+ as
+ Begin
+ return replicate(@PadChar,@len-Len(@value))+@value
+ end
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/possibleNewMissions.sql b/docs/db_structure/functions/possibleNewMissions.sql
new file mode 100644
index 0000000..58aab25
--- /dev/null
+++ b/docs/db_structure/functions/possibleNewMissions.sql
@@ -0,0 +1,23 @@
+/****** Object: UserDefinedFunction [dbo].[possibleNewMissions] Script Date: 10.05.2026 10:10:00 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[possibleNewMissions]
+(
+
+)
+RETURNS TABLE
+AS
+RETURN
+(
+SELECT * FROM missions WHERE missionlevel=-1 AND listable=1
+UNION
+SELECT * FROM dbo.liveRandomMissions()
+UNION
+SELECT * FROM missions WHERE missiontype=12
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/productionFacilitiesByPattern.sql b/docs/db_structure/functions/productionFacilitiesByPattern.sql
new file mode 100644
index 0000000..13e4d49
--- /dev/null
+++ b/docs/db_structure/functions/productionFacilitiesByPattern.sql
@@ -0,0 +1,38 @@
+/****** Object: UserDefinedFunction [dbo].[productionFacilitiesByPattern] Script Date: 10.05.2026 10:10:39 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[productionFacilitiesByPattern]
+(
+ @pattern VARCHAR(50)
+)
+RETURNS
+@facilities TABLE
+(
+ [definition] int
+
+)
+AS
+BEGIN
+
+SET @pattern = '%' + @pattern + '%';
+
+INSERT @facilities
+ ( [definition] )
+SELECT d.[definition] FROM dbo.entitydefaults d WHERE d.[definition] IN (SELECT definition FROM dbo.getDefinitionByCFString('cf_production_facilities'))
+AND
+(
+d.definitionname LIKE '%basic%' OR
+d.definitionname LIKE '%advanced%' OR
+d.definitionname LIKE '%expert%' OR
+d.definitionname LIKE '%super%')
+AND d.definitionname NOT LIKE '%insurance%'
+AND d.definitionname LIKE @pattern
+
+ RETURN
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/productionFacilityByLevel.sql b/docs/db_structure/functions/productionFacilityByLevel.sql
new file mode 100644
index 0000000..7be40ac
--- /dev/null
+++ b/docs/db_structure/functions/productionFacilityByLevel.sql
@@ -0,0 +1,27 @@
+/****** Object: UserDefinedFunction [dbo].[productionFacilityByLevel] Script Date: 10.05.2026 10:53:15 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[productionFacilityByLevel]
+(
+ @pattern VARCHAR(50),
+ @level VARCHAR(50)
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result int
+
+ SET @pattern = '%' + @pattern + '%';
+
+ SELECT @Result= [definition] FROM dbo.facilitymap WHERE leveltag=@level AND defname LIKE @pattern
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/productionFacilityFromBaseByPattern.sql b/docs/db_structure/functions/productionFacilityFromBaseByPattern.sql
new file mode 100644
index 0000000..505a13d
--- /dev/null
+++ b/docs/db_structure/functions/productionFacilityFromBaseByPattern.sql
@@ -0,0 +1,23 @@
+/****** Object: UserDefinedFunction [dbo].[productionFacilityFromBaseByPattern] Script Date: 10.05.2026 10:53:56 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[productionFacilityFromBaseByPattern]
+(
+ @baseEid BIGINT,
+ @pattern VARCHAR(50)
+)
+RETURNS BIGINT
+AS
+BEGIN
+
+ DECLARE @Result BIGINT
+ SELECT @Result =eid FROM dbo.entities e WHERE e.parent=@baseEid AND e.[definition] IN (SELECT [definition] FROM dbo.productionFacilitiesByPattern(@pattern))
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/randomHyp.sql b/docs/db_structure/functions/randomHyp.sql
new file mode 100644
index 0000000..6cd0c1a
--- /dev/null
+++ b/docs/db_structure/functions/randomHyp.sql
@@ -0,0 +1,30 @@
+/****** Object: UserDefinedFunction [dbo].[randomHyp] Script Date: 10.05.2026 10:54:29 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[randomHyp]
+(
+ @exponentWorst FLOAT,
+ @exponentBest FLOAT,
+ @biasX FLOAT,
+ @biasY FLOAT,
+ @worstValue FLOAT,
+ @bestValue FLOAT
+)
+RETURNS FLOAT
+AS
+BEGIN
+
+ DECLARE @res FLOAT, @rnd FLOAT
+ SET @rnd = (SELECT TOP 1 * FROM dbo.randomView);
+
+ --SET @res = dbo.biasedHyp(@rnd,@exponentWorst,@exponentBest,@biasX,@biasY,@worstValue,@bestValue);
+ SET @res = 0; -- hotfix
+ RETURN @res;
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/robotTemplateContainsEw.sql b/docs/db_structure/functions/robotTemplateContainsEw.sql
new file mode 100644
index 0000000..bd5adbd
--- /dev/null
+++ b/docs/db_structure/functions/robotTemplateContainsEw.sql
@@ -0,0 +1,60 @@
+/****** Object: UserDefinedFunction [dbo].[robotTemplateContainsEw] Script Date: 10.05.2026 10:55:05 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[robotTemplateContainsEw]
+(
+ @robotTemplateId int
+)
+RETURNS int
+AS
+BEGIN
+
+ DECLARE @Result INT, @templateText VARCHAR(MAX)
+ SET @Result = 0
+ SET @templateText = (SELECT [description] FROM dbo.robottemplates WHERE id=@robotTemplateId)
+
+
+ DECLARE @hexNumber VARCHAR(50), @compareThis VARCHAR(50)
+
+ DECLARE TableCursor CURSOR FOR SELECT hexn FROM dbo.ewModulesInHex()
+
+ OPEN TableCursor
+
+ FETCH NEXT FROM TableCursor INTO @hexNumber
+ WHILE @@FETCH_STATUS = 0
+ BEGIN
+ SET @compareThis = '%i' + @hexNumber + '|%'
+
+
+ IF (@templateText LIKE @compareThis)
+ BEGIN
+ SET @Result = 1;
+ end
+
+ SET @compareThis = '%i' + @hexNumber
+ IF (@templateText LIKE @compareThis)
+ BEGIN
+ SET @Result = 1;
+ end
+
+
+ FETCH NEXT FROM TableCursor INTO @hexNumber
+ END
+
+ CLOSE TableCursor
+
+ DEALLOCATE TableCursor
+
+
+
+
+ -- Return the result of the function
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/sha1FromVarchar.sql b/docs/db_structure/functions/sha1FromVarchar.sql
new file mode 100644
index 0000000..6765f31
--- /dev/null
+++ b/docs/db_structure/functions/sha1FromVarchar.sql
@@ -0,0 +1,21 @@
+/****** Object: UserDefinedFunction [dbo].[sha1FromVarchar] Script Date: 10.05.2026 10:55:42 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[sha1FromVarchar]
+(
+ @input VARCHAR(max)
+)
+RETURNS VARCHAR(100)
+AS
+BEGIN
+ DECLARE @Result VARCHAR(100);
+ SELECT @Result = LOWER(CONVERT(VARCHAR(100), HASHBYTES('SHA1', @input) ,2));
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/showDynProp.sql b/docs/db_structure/functions/showDynProp.sql
new file mode 100644
index 0000000..1a83278
--- /dev/null
+++ b/docs/db_structure/functions/showDynProp.sql
@@ -0,0 +1,56 @@
+/****** Object: UserDefinedFunction [dbo].[showDynProp] Script Date: 10.05.2026 10:56:17 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+/*
+offsetWithinDay=i5
+armor=tD7BDF13E
+constructionLevelCurrent=i1f4
+creation=d2015.2.20.13.46.15
+reinforceCounter=i2
+nextReinforceIncrease=d2015.12.23.2.15.49
+constructionDirection=i1
+isOnline=i1
+isReinforced=i1
+reinforceEnd=d2015.12.23.2.14.49
+*/
+
+
+CREATE FUNCTION [dbo].[showDynProp]
+(
+ @dynProp VARCHAR(max)
+)
+RETURNS VARCHAR(max)
+AS
+BEGIN
+
+ DECLARE @result VARCHAR(max);
+ DECLARE @piece VARCHAR(max);
+ SET @result = '';
+
+ DECLARE pieces CURSOR LOCAL FORWARD_ONLY READ_ONLY FOR
+ SELECT s.value FROM dbo.splitString(@dynProp,'#') s
+ WHERE s.value NOT LIKE '%armor%'
+ AND s.value NOT LIKE '%const%'
+ AND s.value NOT LIKE '%creation%'
+ ;
+ OPEN pieces
+ FETCH NEXT FROM pieces INTO @piece
+ WHILE (@@FETCH_STATUS = 0)
+ BEGIN
+
+ SET @result = @result + @piece + CHAR(13)+CHAR(10) ;
+
+ FETCH NEXT FROM pieces INTO @piece
+ END
+
+
+ CLOSE pieces; DEALLOCATE pieces;
+
+ RETURN @result;
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/splitString.sql b/docs/db_structure/functions/splitString.sql
new file mode 100644
index 0000000..f5918ee
--- /dev/null
+++ b/docs/db_structure/functions/splitString.sql
@@ -0,0 +1,47 @@
+/****** Object: UserDefinedFunction [dbo].[splitString] Script Date: 10.05.2026 10:11:10 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+create FUNCTION [dbo].[splitString]
+(
+ @input as varchar(max),
+ @delimiter as varchar(10) = ","
+)
+RETURNS
+@result TABLE
+(
+ id INT identity(1,1),
+ value varchar(max) not null
+)
+AS
+BEGIN
+
+ DECLARE @pos AS INT;
+ DECLARE @string AS VARCHAR(MAX) = '';
+
+ WHILE LEN(@input) > 0
+ BEGIN
+ SELECT @pos = CHARINDEX(@delimiter,@input);
+
+ IF(@pos<=0)
+ select @pos = len(@input)
+
+ IF(@pos <> LEN(@input))
+ SELECT @string = SUBSTRING(@input, 1, @pos-1);
+ ELSE
+ SELECT @string = SUBSTRING(@input, 1, @pos);
+
+ INSERT INTO @result SELECT @string
+
+ SELECT @input = SUBSTRING(@input, @pos+len(@delimiter), LEN(@input)-@pos)
+ END
+
+
+ RETURN
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/stringToInsertString.sql b/docs/db_structure/functions/stringToInsertString.sql
new file mode 100644
index 0000000..d71e293
--- /dev/null
+++ b/docs/db_structure/functions/stringToInsertString.sql
@@ -0,0 +1,32 @@
+/****** Object: UserDefinedFunction [dbo].[stringToInsertString] Script Date: 10.05.2026 10:56:57 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+create FUNCTION [dbo].[stringToInsertString]
+(
+ @p1 VARCHAR(MAX)
+)
+RETURNS VARCHAR(MAX)
+AS
+BEGIN
+ DECLARE @Result VARCHAR(MAX)
+
+ IF (@p1 IS NULL)
+ BEGIN
+ SET @Result = 'NULL'
+ END
+ ELSE
+ BEGIN
+ SET @Result = '''' + @p1 + ''''
+ END
+
+ SET @Result = RTRIM(LTRIM(@Result))
+
+ RETURN @Result
+
+END
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/teleportColumns.sql b/docs/db_structure/functions/teleportColumns.sql
new file mode 100644
index 0000000..7a38969
--- /dev/null
+++ b/docs/db_structure/functions/teleportColumns.sql
@@ -0,0 +1,26 @@
+/****** Object: UserDefinedFunction [dbo].[teleportColumns] Script Date: 10.05.2026 10:11:43 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[teleportColumns]
+(
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+SELECT DISTINCT tpcEid FROM
+(
+SELECT sourcecolumn AS tpcEid FROM dbo.teleportdescriptions
+UNION
+SELECT targetcolumn FROM dbo.teleportdescriptions
+) AS kupac
+WHERE kupac.tpcEid IS NOT NULL
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/treeEids.sql b/docs/db_structure/functions/treeEids.sql
new file mode 100644
index 0000000..b1240d4
--- /dev/null
+++ b/docs/db_structure/functions/treeEids.sql
@@ -0,0 +1,33 @@
+/****** Object: UserDefinedFunction [dbo].[treeEids] Script Date: 10.05.2026 10:12:16 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+--except root
+
+CREATE FUNCTION [dbo].[treeEids]
+(
+ @rootEID bigint
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+with children(eid)
+ as
+ (
+ SELECT eid FROM entities WHERE eid = @rootEID
+
+ UNION ALL
+
+ SELECT C.eid FROM entities AS C JOIN children AS M ON C.parent = M.eid
+ )
+
+select eid from children where eid<>@rootEID
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/tutorialMissionTargetsOnZone.sql b/docs/db_structure/functions/tutorialMissionTargetsOnZone.sql
new file mode 100644
index 0000000..28149c0
--- /dev/null
+++ b/docs/db_structure/functions/tutorialMissionTargetsOnZone.sql
@@ -0,0 +1,37 @@
+/****** Object: UserDefinedFunction [dbo].[tutorialMissionTargetsOnZone] Script Date: 10.05.2026 10:12:54 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+CREATE FUNCTION [dbo].[tutorialMissionTargetsOnZone]
+(
+ @zoneId INT
+)
+RETURNS TABLE
+AS
+RETURN
+(
+
+ SELECT * FROM dbo.missiontargets WHERE missionid IN
+ (
+ SELECT id FROM missions WHERE sourceagent in
+ (SELECT agentid FROM dbo.missionLocationsFromZone(@zoneId))
+ AND missionlevel=-1
+ AND listable=1
+ )
+
+ EXCEPT
+
+ SELECT * FROM dbo.missiontargets WHERE missionid IN
+ (
+ SELECT id FROM missions WHERE sourceagent in
+ (SELECT agentid FROM dbo.missionLocationsFromZone(@zoneId))
+ AND missionlevel !=-1
+ AND listable =1
+ )
+
+)
+GO
\ No newline at end of file
diff --git a/docs/db_structure/functions/tutorialStructures.sql b/docs/db_structure/functions/tutorialStructures.sql
new file mode 100644
index 0000000..40c9394
--- /dev/null
+++ b/docs/db_structure/functions/tutorialStructures.sql
@@ -0,0 +1,39 @@
+/****** Object: UserDefinedFunction [dbo].[tutorialStructures] Script Date: 10.05.2026 10:13:30 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+/*
+submit_item= 9,
+use_switch= 10,
+use_itemsupply = 13,
+*/
+
+-- from all zones, keep it simple
+
+CREATE FUNCTION [dbo].[tutorialStructures] ()
+RETURNS TABLE
+AS
+RETURN
+(
+
+SELECT eid FROM dbo.zoneentities ze WHERE ze.eid in
+(
+ SELECT structureeid FROM dbo.missiontargets WHERE
+ structureeid IS NOT NULL and
+ targettype IN (9,10,13) AND
+ missionid IN
+ (
+ SELECT id FROM missions WHERE missionlevel=-1 AND listable=1 --tutorial missions
+ )
+)
+AND --ja meg meg structure is
+ze.definition IN (SELECT ze.definition FROM dbo.getDefinitionByCFString('cf_mission_structures'))
+)
+
+
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/stored_procedures/dbo.CreateFolderContainer.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.CreateFolderContainer.StoredProcedure.sql
new file mode 100644
index 0000000..c42494c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.CreateFolderContainer.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.DeleteAllGang.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.DeleteAllGang.StoredProcedure.sql
new file mode 100644
index 0000000..f96b085
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.DeleteAllGang.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.DuplicateDefinitionConfig.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.DuplicateDefinitionConfig.StoredProcedure.sql
new file mode 100644
index 0000000..233a6db
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.DuplicateDefinitionConfig.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.EpForActivityByType.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.EpForActivityByType.StoredProcedure.sql
new file mode 100644
index 0000000..4c7a000
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.EpForActivityByType.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.GetNpcKillEp.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.GetNpcKillEp.StoredProcedure.sql
new file mode 100644
index 0000000..613e80e
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.GetNpcKillEp.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.PrintForeignKeys.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.PrintForeignKeys.StoredProcedure.sql
new file mode 100644
index 0000000..2479d25
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.PrintForeignKeys.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.UpdateNPCKills.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.UpdateNPCKills.StoredProcedure.sql
new file mode 100644
index 0000000..32bc634
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.UpdateNPCKills.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountAddCredit.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountAddCredit.StoredProcedure.sql
new file mode 100644
index 0000000..5c5ffb7
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountAddCredit.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountAllocateSteamKey.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountAllocateSteamKey.StoredProcedure.sql
new file mode 100644
index 0000000..6582512
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountAllocateSteamKey.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountCreditEnqueue.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountCreditEnqueue.StoredProcedure.sql
new file mode 100644
index 0000000..7f11c8a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountCreditEnqueue.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountEmailConfirm.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountEmailConfirm.StoredProcedure.sql
new file mode 100644
index 0000000..1133142
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountEmailConfirm.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPackageBought.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPackageBought.StoredProcedure.sql
new file mode 100644
index 0000000..6f128e3
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPackageBought.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPackageGenerateAll.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPackageGenerateAll.StoredProcedure.sql
new file mode 100644
index 0000000..b100f39
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPackageGenerateAll.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPackageGenerateSpark.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPackageGenerateSpark.StoredProcedure.sql
new file mode 100644
index 0000000..fe2a3a4
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPackageGenerateSpark.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPackageHas.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPackageHas.StoredProcedure.sql
new file mode 100644
index 0000000..8592daa
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPackageHas.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPackageList.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPackageList.StoredProcedure.sql
new file mode 100644
index 0000000..ff071d8
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPackageList.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPackageProcessOne.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPackageProcessOne.StoredProcedure.sql
new file mode 100644
index 0000000..2ec2cb5
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPackageProcessOne.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountPurchase.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountPurchase.StoredProcedure.sql
new file mode 100644
index 0000000..a677cd8
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountPurchase.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountSimpleDelete.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountSimpleDelete.StoredProcedure.sql
new file mode 100644
index 0000000..fcd6a38
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountSimpleDelete.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountonlinetimestart.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountonlinetimestart.StoredProcedure.sql
new file mode 100644
index 0000000..8e8be22
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountonlinetimestart.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.accountonlinetimestop.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.accountonlinetimestop.StoredProcedure.sql
new file mode 100644
index 0000000..8f878af
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.accountonlinetimestop.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addCampaignGoodiePack.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addCampaignGoodiePack.StoredProcedure.sql
new file mode 100644
index 0000000..01593d7
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addCampaignGoodiePack.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addDefinitionToBetaMarkets.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addDefinitionToBetaMarkets.StoredProcedure.sql
new file mode 100644
index 0000000..d025792
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addDefinitionToBetaMarkets.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addDefinitionToDefaultMarkets.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addDefinitionToDefaultMarkets.StoredProcedure.sql
new file mode 100644
index 0000000..143405d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addDefinitionToDefaultMarkets.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addIP.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addIP.StoredProcedure.sql
new file mode 100644
index 0000000..e4a83cb
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addIP.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addKill.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addKill.StoredProcedure.sql
new file mode 100644
index 0000000..2380333
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addKill.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addVendorBuyItem.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addVendorBuyItem.StoredProcedure.sql
new file mode 100644
index 0000000..854e60b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addVendorBuyItem.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addVendorSellItem.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addVendorSellItem.StoredProcedure.sql
new file mode 100644
index 0000000..d1574b5
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addVendorSellItem.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addownerincome.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addownerincome.StoredProcedure.sql
new file mode 100644
index 0000000..e59dd10
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addownerincome.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.addpresettorandompool.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.addpresettorandompool.StoredProcedure.sql
new file mode 100644
index 0000000..4aae7ed
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.addpresettorandompool.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.artifactReset.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.artifactReset.StoredProcedure.sql
new file mode 100644
index 0000000..4eb272f
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.artifactReset.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.backupCurrentDbFull.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.backupCurrentDbFull.StoredProcedure.sql
new file mode 100644
index 0000000..e71a749
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.backupCurrentDbFull.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.backupLog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.backupLog.StoredProcedure.sql
new file mode 100644
index 0000000..5a5b692
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.backupLog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.centralBank_add.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.centralBank_add.StoredProcedure.sql
new file mode 100644
index 0000000..19648af
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.centralBank_add.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.centralBank_addLog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.centralBank_addLog.StoredProcedure.sql
new file mode 100644
index 0000000..80bba25
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.centralBank_addLog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.centralBank_sub.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.centralBank_sub.StoredProcedure.sql
new file mode 100644
index 0000000..5ed3e46
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.centralBank_sub.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.changeEIDinOneTable.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.changeEIDinOneTable.StoredProcedure.sql
new file mode 100644
index 0000000..3658a16
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.changeEIDinOneTable.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.changeFacilities.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.changeFacilities.StoredProcedure.sql
new file mode 100644
index 0000000..4bf6eda
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.changeFacilities.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.channelAddBan.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.channelAddBan.StoredProcedure.sql
new file mode 100644
index 0000000..0e4086d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.channelAddBan.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.channelMemberAdd.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.channelMemberAdd.StoredProcedure.sql
new file mode 100644
index 0000000..f41c54a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.channelMemberAdd.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.characterDockToTMA.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.characterDockToTMA.StoredProcedure.sql
new file mode 100644
index 0000000..0ff3bad
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.characterDockToTMA.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.characterSettingsSetString.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.characterSettingsSetString.StoredProcedure.sql
new file mode 100644
index 0000000..29d69bc
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.characterSettingsSetString.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.checkGameConsistency.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.checkGameConsistency.StoredProcedure.sql
new file mode 100644
index 0000000..e7bbd91
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.checkGameConsistency.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.checkSyntaxInAllCode.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.checkSyntaxInAllCode.StoredProcedure.sql
new file mode 100644
index 0000000..d632965
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.checkSyntaxInAllCode.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.cleanDisabledDefinitions.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.cleanDisabledDefinitions.StoredProcedure.sql
new file mode 100644
index 0000000..1f1b734
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.cleanDisabledDefinitions.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.cleanUpGame.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.cleanUpGame.StoredProcedure.sql
new file mode 100644
index 0000000..d436a5b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.cleanUpGame.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.cleanUpInsuranceByBaseEid.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.cleanUpInsuranceByBaseEid.StoredProcedure.sql
new file mode 100644
index 0000000..97e3900
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.cleanUpInsuranceByBaseEid.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.cleanUpMarket.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.cleanUpMarket.StoredProcedure.sql
new file mode 100644
index 0000000..5d46d34
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.cleanUpMarket.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.cleanUpZoneUserEntities.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.cleanUpZoneUserEntities.StoredProcedure.sql
new file mode 100644
index 0000000..ee14f44
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.cleanUpZoneUserEntities.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.consolidate_statistics.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.consolidate_statistics.StoredProcedure.sql
new file mode 100644
index 0000000..b251883
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.consolidate_statistics.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.copyArtifactLoot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.copyArtifactLoot.StoredProcedure.sql
new file mode 100644
index 0000000..1e4e1d4
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.copyArtifactLoot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.copyComponents.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.copyComponents.StoredProcedure.sql
new file mode 100644
index 0000000..92a7dcd
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.copyComponents.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.copyLVL1ArtifactLoot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.copyLVL1ArtifactLoot.StoredProcedure.sql
new file mode 100644
index 0000000..5861079
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.copyLVL1ArtifactLoot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.copyLVL3ArtifactLoot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.copyLVL3ArtifactLoot.StoredProcedure.sql
new file mode 100644
index 0000000..6e1db90
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.copyLVL3ArtifactLoot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.copycharacterwizard.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.copycharacterwizard.StoredProcedure.sql
new file mode 100644
index 0000000..10353e1
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.copycharacterwizard.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.countParents.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.countParents.StoredProcedure.sql
new file mode 100644
index 0000000..184017a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.countParents.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.createVendor.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.createVendor.StoredProcedure.sql
new file mode 100644
index 0000000..72ed05b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.createVendor.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_crm.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_crm.StoredProcedure.sql
new file mode 100644
index 0000000..50989bf
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_crm.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_devserver.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_devserver.StoredProcedure.sql
new file mode 100644
index 0000000..671c982
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_devserver.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_disablenpcs.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_disablenpcs.StoredProcedure.sql
new file mode 100644
index 0000000..2b3cdfa
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_disablenpcs.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_disablezones.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_disablezones.StoredProcedure.sql
new file mode 100644
index 0000000..e90b73a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_disablezones.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_enableallzones.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_enableallzones.StoredProcedure.sql
new file mode 100644
index 0000000..3f87971
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_enableallzones.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_enablenpcs.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_enablenpcs.StoredProcedure.sql
new file mode 100644
index 0000000..74e0e3b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_enablenpcs.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_junior.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_junior.StoredProcedure.sql
new file mode 100644
index 0000000..2a0c60f
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_junior.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.debug_liveserver.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.debug_liveserver.StoredProcedure.sql
new file mode 100644
index 0000000..254b424
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.debug_liveserver.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.deleteAllChildren.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.deleteAllChildren.StoredProcedure.sql
new file mode 100644
index 0000000..7766ce7
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.deleteAllChildren.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.deleteTree.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.deleteTree.StoredProcedure.sql
new file mode 100644
index 0000000..f48bfeb
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.deleteTree.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.deleteUnusedPublicChannels.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.deleteUnusedPublicChannels.StoredProcedure.sql
new file mode 100644
index 0000000..2f65e7a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.deleteUnusedPublicChannels.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.dockInCharacterAndRobot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.dockInCharacterAndRobot.StoredProcedure.sql
new file mode 100644
index 0000000..440417a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.dockInCharacterAndRobot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.entitiesFixParenting.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.entitiesFixParenting.StoredProcedure.sql
new file mode 100644
index 0000000..82e5495
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.entitiesFixParenting.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.entitiesReportAndDeleteOrphanedByCf.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.entitiesReportAndDeleteOrphanedByCf.StoredProcedure.sql
new file mode 100644
index 0000000..0573b9d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.entitiesReportAndDeleteOrphanedByCf.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.epForActivityLogList.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.epForActivityLogList.StoredProcedure.sql
new file mode 100644
index 0000000..aa84a23
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.epForActivityLogList.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionPointsAdd.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionPointsAdd.StoredProcedure.sql
new file mode 100644
index 0000000..ccd0115
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionPointsAdd.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionPointsCheck.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionPointsCheck.StoredProcedure.sql
new file mode 100644
index 0000000..0a2de7e
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionPointsCheck.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionPointsConsolidate.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionPointsConsolidate.StoredProcedure.sql
new file mode 100644
index 0000000..6fea771
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionPointsConsolidate.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionPointsInject.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionPointsInject.StoredProcedure.sql
new file mode 100644
index 0000000..5d03632
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionPointsInject.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionRevert.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionRevert.StoredProcedure.sql
new file mode 100644
index 0000000..040645c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionRevert.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionRevertV2.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionRevertV2.StoredProcedure.sql
new file mode 100644
index 0000000..71d628d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionRevertV2.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionSubscriptionStart.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionSubscriptionStart.StoredProcedure.sql
new file mode 100644
index 0000000..73c1ed9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionSubscriptionStart.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.extensionsRevertFromDate.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.extensionsRevertFromDate.StoredProcedure.sql
new file mode 100644
index 0000000..1be8507
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.extensionsRevertFromDate.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.fieldTerminal_itemCount.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.fieldTerminal_itemCount.StoredProcedure.sql
new file mode 100644
index 0000000..cdd73a2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.fieldTerminal_itemCount.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.findContainerRoot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.findContainerRoot.StoredProcedure.sql
new file mode 100644
index 0000000..69c9f21
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.findContainerRoot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.freshNewsCount.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.freshNewsCount.StoredProcedure.sql
new file mode 100644
index 0000000..3d01863
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.freshNewsCount.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getAllChildrenEids.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getAllChildrenEids.StoredProcedure.sql
new file mode 100644
index 0000000..f4ce1ee
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getAllChildrenEids.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getEntityInfo.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getEntityInfo.StoredProcedure.sql
new file mode 100644
index 0000000..5955930
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getEntityInfo.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getExtensionBonus.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getExtensionBonus.StoredProcedure.sql
new file mode 100644
index 0000000..f035731
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getExtensionBonus.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getFullRobot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getFullRobot.StoredProcedure.sql
new file mode 100644
index 0000000..0b2d1a0
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getFullRobot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getFullTree.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getFullTree.StoredProcedure.sql
new file mode 100644
index 0000000..a0f7d65
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getFullTree.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getFullTreeByRoot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getFullTreeByRoot.StoredProcedure.sql
new file mode 100644
index 0000000..f8647bb
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getFullTreeByRoot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getItemSummary.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getItemSummary.StoredProcedure.sql
new file mode 100644
index 0000000..69cfaa2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getItemSummary.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getList.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getList.StoredProcedure.sql
new file mode 100644
index 0000000..a2c1f6b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getList.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getList2.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getList2.StoredProcedure.sql
new file mode 100644
index 0000000..eaae85a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getList2.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getMissionAverageTime.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getMissionAverageTime.StoredProcedure.sql
new file mode 100644
index 0000000..3b15222
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getMissionAverageTime.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getModulesFromRobot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getModulesFromRobot.StoredProcedure.sql
new file mode 100644
index 0000000..2b48506
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getModulesFromRobot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getStructureRoot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getStructureRoot.StoredProcedure.sql
new file mode 100644
index 0000000..5db0686
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getStructureRoot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getTableColumnInfo.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getTableColumnInfo.StoredProcedure.sql
new file mode 100644
index 0000000..47076b0
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getTableColumnInfo.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getTreeEIDs.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getTreeEIDs.StoredProcedure.sql
new file mode 100644
index 0000000..b132642
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getTreeEIDs.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getTreeNonFiltered.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getTreeNonFiltered.StoredProcedure.sql
new file mode 100644
index 0000000..1e26cb5
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getTreeNonFiltered.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.getcorporationstrength.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.getcorporationstrength.StoredProcedure.sql
new file mode 100644
index 0000000..0548698
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.getcorporationstrength.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.increaseExtensionLevel.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.increaseExtensionLevel.StoredProcedure.sql
new file mode 100644
index 0000000..509c2a2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.increaseExtensionLevel.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.indexesMaintenance.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.indexesMaintenance.StoredProcedure.sql
new file mode 100644
index 0000000..57715d4
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.indexesMaintenance.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.initServer.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.initServer.StoredProcedure.sql
new file mode 100644
index 0000000..9ab0d82
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.initServer.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.initchannels.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.initchannels.StoredProcedure.sql
new file mode 100644
index 0000000..4b56caa
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.initchannels.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.insertAveragePrice.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.insertAveragePrice.StoredProcedure.sql
new file mode 100644
index 0000000..6766b04
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.insertAveragePrice.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.insertMarketItem.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.insertMarketItem.StoredProcedure.sql
new file mode 100644
index 0000000..24f6f47
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.insertMarketItem.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.insuranceadd.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.insuranceadd.StoredProcedure.sql
new file mode 100644
index 0000000..b0e2285
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.insuranceadd.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.itemCount.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.itemCount.StoredProcedure.sql
new file mode 100644
index 0000000..9ab0107
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.itemCount.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.itemCountOnBases.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.itemCountOnBases.StoredProcedure.sql
new file mode 100644
index 0000000..0092775
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.itemCountOnBases.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.itemCountOnZone.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.itemCountOnZone.StoredProcedure.sql
new file mode 100644
index 0000000..8427c98
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.itemCountOnZone.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.itemscore_write.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.itemscore_write.StoredProcedure.sql
new file mode 100644
index 0000000..7d21ca9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.itemscore_write.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.listMarketAverageByDefinition.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.listMarketAverageByDefinition.StoredProcedure.sql
new file mode 100644
index 0000000..2c3cf79
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.listMarketAverageByDefinition.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.listTeleportColumns.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.listTeleportColumns.StoredProcedure.sql
new file mode 100644
index 0000000..d6add93
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.listTeleportColumns.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.m_fix_issuers.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.m_fix_issuers.StoredProcedure.sql
new file mode 100644
index 0000000..a3b16fa
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.m_fix_issuers.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.maildelete.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.maildelete.StoredProcedure.sql
new file mode 100644
index 0000000..f7e303c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.maildelete.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.mailopen.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.mailopen.StoredProcedure.sql
new file mode 100644
index 0000000..d6e1c81
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.mailopen.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.mailsend.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.mailsend.StoredProcedure.sql
new file mode 100644
index 0000000..92f731f
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.mailsend.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.maintainHealth.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.maintainHealth.StoredProcedure.sql
new file mode 100644
index 0000000..7534a67
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.maintainHealth.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.marketAddAllItemsDev.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.marketAddAllItemsDev.StoredProcedure.sql
new file mode 100644
index 0000000..d7ba9ad
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.marketAddAllItemsDev.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.marketGetOrdersForCancel.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.marketGetOrdersForCancel.StoredProcedure.sql
new file mode 100644
index 0000000..f78ef2b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.marketGetOrdersForCancel.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.marketfill.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.marketfill.StoredProcedure.sql
new file mode 100644
index 0000000..2639f19
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.marketfill.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionAddParticipant.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionAddParticipant.StoredProcedure.sql
new file mode 100644
index 0000000..8c9f1ba
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionAddParticipant.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionAfterSync.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionAfterSync.StoredProcedure.sql
new file mode 100644
index 0000000..14176cb
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionAfterSync.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionAgentDelete.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionAgentDelete.StoredProcedure.sql
new file mode 100644
index 0000000..b98f61c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionAgentDelete.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionBonusWrite.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionBonusWrite.StoredProcedure.sql
new file mode 100644
index 0000000..8f86ba2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionBonusWrite.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionCleanUpLog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionCleanUpLog.StoredProcedure.sql
new file mode 100644
index 0000000..1d94358
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionCleanUpLog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionDeleteLogInChunks.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionDeleteLogInChunks.StoredProcedure.sql
new file mode 100644
index 0000000..80dafc9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionDeleteLogInChunks.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionPrepareSync.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionPrepareSync.StoredProcedure.sql
new file mode 100644
index 0000000..cf2fe99
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionPrepareSync.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.missionRemoveByAgent.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.missionRemoveByAgent.StoredProcedure.sql
new file mode 100644
index 0000000..9ed9764
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.missionRemoveByAgent.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.mission_writetargetinprogress.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.mission_writetargetinprogress.StoredProcedure.sql
new file mode 100644
index 0000000..fa67a47
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.mission_writetargetinprogress.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.movecontainerstoroot.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.movecontainerstoroot.StoredProcedure.sql
new file mode 100644
index 0000000..06719cc
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.movecontainerstoroot.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.newAccountBySteamID.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.newAccountBySteamID.StoredProcedure.sql
new file mode 100644
index 0000000..5c10519
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.newAccountBySteamID.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.newMailCount.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.newMailCount.StoredProcedure.sql
new file mode 100644
index 0000000..984f772
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.newMailCount.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.newMassMailCount.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.newMassMailCount.StoredProcedure.sql
new file mode 100644
index 0000000..08ebbc1
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.newMassMailCount.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.pbsDeleteTurretChildren.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.pbsDeleteTurretChildren.StoredProcedure.sql
new file mode 100644
index 0000000..e14a6cc
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.pbsDeleteTurretChildren.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.pbsTrashCleanUp.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.pbsTrashCleanUp.StoredProcedure.sql
new file mode 100644
index 0000000..5115742
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.pbsTrashCleanUp.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.printPollResult.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.printPollResult.StoredProcedure.sql
new file mode 100644
index 0000000..5d404ea
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.printPollResult.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.querystatistics.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.querystatistics.StoredProcedure.sql
new file mode 100644
index 0000000..ba548f2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.querystatistics.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.rebuildIndexesByThreshold.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.rebuildIndexesByThreshold.StoredProcedure.sql
new file mode 100644
index 0000000..c737e24
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.rebuildIndexesByThreshold.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.rebuildTableIndices.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.rebuildTableIndices.StoredProcedure.sql
new file mode 100644
index 0000000..35329fe
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.rebuildTableIndices.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.recalculateVendorPrices.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.recalculateVendorPrices.StoredProcedure.sql
new file mode 100644
index 0000000..a58d64c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.recalculateVendorPrices.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.recalculate_raw_material_prices.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.recalculate_raw_material_prices.StoredProcedure.sql
new file mode 100644
index 0000000..6eab078
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.recalculate_raw_material_prices.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.reimburseFromCharacterTransactions.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.reimburseFromCharacterTransactions.StoredProcedure.sql
new file mode 100644
index 0000000..0ac8339
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.reimburseFromCharacterTransactions.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.reimbursepbs.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.reimbursepbs.StoredProcedure.sql
new file mode 100644
index 0000000..21589ba
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.reimbursepbs.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.reorganizeIndexesByThreshold.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.reorganizeIndexesByThreshold.StoredProcedure.sql
new file mode 100644
index 0000000..460029b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.reorganizeIndexesByThreshold.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.reorganizeTableIndices.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.reorganizeTableIndices.StoredProcedure.sql
new file mode 100644
index 0000000..56e4fb3
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.reorganizeTableIndices.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_AutoGrowthEvents.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_AutoGrowthEvents.StoredProcedure.sql
new file mode 100644
index 0000000..7f86bef
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_AutoGrowthEvents.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_GMOnlineTimes.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_GMOnlineTimes.StoredProcedure.sql
new file mode 100644
index 0000000..fb3144d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_GMOnlineTimes.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_MarketBuyByDefinition.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_MarketBuyByDefinition.StoredProcedure.sql
new file mode 100644
index 0000000..7ff0bfe
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_MarketBuyByDefinition.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_centralbanklog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_centralbanklog.StoredProcedure.sql
new file mode 100644
index 0000000..966617f
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_centralbanklog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_corpCEOs.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_corpCEOs.StoredProcedure.sql
new file mode 100644
index 0000000..7cde2df
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_corpCEOs.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_currentlyBlockingQueries.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_currentlyBlockingQueries.StoredProcedure.sql
new file mode 100644
index 0000000..818b769
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_currentlyBlockingQueries.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_decorTopList.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_decorTopList.StoredProcedure.sql
new file mode 100644
index 0000000..ce3a7d2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_decorTopList.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_hardwareinfo.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_hardwareinfo.StoredProcedure.sql
new file mode 100644
index 0000000..8d78605
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_hardwareinfo.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_indexUsage.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_indexUsage.StoredProcedure.sql
new file mode 100644
index 0000000..8e58433
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_indexUsage.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_insurencePayout.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_insurencePayout.StoredProcedure.sql
new file mode 100644
index 0000000..5dcc21d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_insurencePayout.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_intel.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_intel.StoredProcedure.sql
new file mode 100644
index 0000000..33f2a91
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_intel.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_itemStock.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_itemStock.StoredProcedure.sql
new file mode 100644
index 0000000..032b437
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_itemStock.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_locks.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_locks.StoredProcedure.sql
new file mode 100644
index 0000000..80e5fd9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_locks.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_missionAvgLength.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_missionAvgLength.StoredProcedure.sql
new file mode 100644
index 0000000..619f037
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_missionAvgLength.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_missionTimes.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_missionTimes.StoredProcedure.sql
new file mode 100644
index 0000000..f836131
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_missionTimes.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_noralgisseedbuy.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_noralgisseedbuy.StoredProcedure.sql
new file mode 100644
index 0000000..5db889c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_noralgisseedbuy.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_onlineTrialCharacters.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_onlineTrialCharacters.StoredProcedure.sql
new file mode 100644
index 0000000..66ec850
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_onlineTrialCharacters.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_planUsage.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_planUsage.StoredProcedure.sql
new file mode 100644
index 0000000..d87c3ed
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_planUsage.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_probePlacement.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_probePlacement.StoredProcedure.sql
new file mode 100644
index 0000000..5618d0c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_probePlacement.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_production_prototype_by_definition.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_production_prototype_by_definition.StoredProcedure.sql
new file mode 100644
index 0000000..cf658cb
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_production_prototype_by_definition.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_pvpvspve.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_pvpvspve.StoredProcedure.sql
new file mode 100644
index 0000000..6eb9cf1
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_pvpvspve.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_queryPerformance.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_queryPerformance.StoredProcedure.sql
new file mode 100644
index 0000000..c171be9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_queryPerformance.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_tableSizes.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_tableSizes.StoredProcedure.sql
new file mode 100644
index 0000000..12b4407
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_tableSizes.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_topitemsbought.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_topitemsbought.StoredProcedure.sql
new file mode 100644
index 0000000..fbde402
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_topitemsbought.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_totalproduction.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_totalproduction.StoredProcedure.sql
new file mode 100644
index 0000000..d6b10ab
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_totalproduction.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.report_traceroute.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.report_traceroute.StoredProcedure.sql
new file mode 100644
index 0000000..7b167b4
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.report_traceroute.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.researchLevelWrite.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.researchLevelWrite.StoredProcedure.sql
new file mode 100644
index 0000000..986ae4d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.researchLevelWrite.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.robotReimburse.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.robotReimburse.StoredProcedure.sql
new file mode 100644
index 0000000..69a0281
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.robotReimburse.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.robotReturn.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.robotReturn.StoredProcedure.sql
new file mode 100644
index 0000000..6a440a0
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.robotReturn.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.runScriptSingleUser.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.runScriptSingleUser.StoredProcedure.sql
new file mode 100644
index 0000000..84b4772
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.runScriptSingleUser.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountBan.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountBan.StoredProcedure.sql
new file mode 100644
index 0000000..950057c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountBan.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountCreate.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountCreate.StoredProcedure.sql
new file mode 100644
index 0000000..5bb02a6
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountCreate.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountDelete.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountDelete.StoredProcedure.sql
new file mode 100644
index 0000000..49e68d5
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountDelete.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountInvalidate.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountInvalidate.StoredProcedure.sql
new file mode 100644
index 0000000..c881285
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountInvalidate.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountSetEmail.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountSetEmail.StoredProcedure.sql
new file mode 100644
index 0000000..023ea32
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountSetEmail.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountSetPassword.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountSetPassword.StoredProcedure.sql
new file mode 100644
index 0000000..4720c4b
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountSetPassword.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountUnBan.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountUnBan.StoredProcedure.sql
new file mode 100644
index 0000000..b242c36
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountUnBan.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saAccountUpdate.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saAccountUpdate.StoredProcedure.sql
new file mode 100644
index 0000000..f09d8e2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saAccountUpdate.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saServerInfoGet.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saServerInfoGet.StoredProcedure.sql
new file mode 100644
index 0000000..b5d16e8
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saServerInfoGet.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.saServerInfoSet.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.saServerInfoSet.StoredProcedure.sql
new file mode 100644
index 0000000..7165439
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.saServerInfoSet.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.savePresetFromCF.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.savePresetFromCF.StoredProcedure.sql
new file mode 100644
index 0000000..8dbf32a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.savePresetFromCF.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_databaseSize.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_databaseSize.StoredProcedure.sql
new file mode 100644
index 0000000..75e6d00
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_databaseSize.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_gotoFieldTerminals.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_gotoFieldTerminals.StoredProcedure.sql
new file mode 100644
index 0000000..7178596
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_gotoFieldTerminals.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_gotoMissionStuctures.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_gotoMissionStuctures.StoredProcedure.sql
new file mode 100644
index 0000000..f3764f9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_gotoMissionStuctures.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_indexDefrag.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_indexDefrag.StoredProcedure.sql
new file mode 100644
index 0000000..87dc4d8
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_indexDefrag.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_listFreeEids.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_listFreeEids.StoredProcedure.sql
new file mode 100644
index 0000000..4d28213
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_listFreeEids.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_rebuildIdentities.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_rebuildIdentities.StoredProcedure.sql
new file mode 100644
index 0000000..32f080d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_rebuildIdentities.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_rebuild_online.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_rebuild_online.StoredProcedure.sql
new file mode 100644
index 0000000..222c160
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_rebuild_online.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_searchInSPCode.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_searchInSPCode.StoredProcedure.sql
new file mode 100644
index 0000000..d30ea50
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_searchInSPCode.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.script_searchStringInData.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.script_searchStringInData.StoredProcedure.sql
new file mode 100644
index 0000000..224c075
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.script_searchStringInData.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setExtensionLevel.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setExtensionLevel.StoredProcedure.sql
new file mode 100644
index 0000000..324858f
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setExtensionLevel.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setManualPrice.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setManualPrice.StoredProcedure.sql
new file mode 100644
index 0000000..794c236
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setManualPrice.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setOwner.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setOwner.StoredProcedure.sql
new file mode 100644
index 0000000..3f79b0c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setOwner.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setOwnerAndReparent.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setOwnerAndReparent.StoredProcedure.sql
new file mode 100644
index 0000000..ba95ec9
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setOwnerAndReparent.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setPrice.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setPrice.StoredProcedure.sql
new file mode 100644
index 0000000..11f1144
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setPrice.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setRelayListening.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setRelayListening.StoredProcedure.sql
new file mode 100644
index 0000000..86d167e
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setRelayListening.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setStanding.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setStanding.StoredProcedure.sql
new file mode 100644
index 0000000..8c10009
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setStanding.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setTreeOwner.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setTreeOwner.StoredProcedure.sql
new file mode 100644
index 0000000..ef2a5d5
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setTreeOwner.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.setTrialFlag.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.setTrialFlag.StoredProcedure.sql
new file mode 100644
index 0000000..8ceeb1f
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.setTrialFlag.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.signIn.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.signIn.StoredProcedure.sql
new file mode 100644
index 0000000..f1bb039
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.signIn.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.sp_RecordPlasmaGathered.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.sp_RecordPlasmaGathered.StoredProcedure.sql
new file mode 100644
index 0000000..480a99a
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.sp_RecordPlasmaGathered.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.sp_RecordPlasmaSold.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.sp_RecordPlasmaSold.StoredProcedure.sql
new file mode 100644
index 0000000..d6dbdd0
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.sp_RecordPlasmaSold.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.sp_RecordResourceGathered.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.sp_RecordResourceGathered.StoredProcedure.sql
new file mode 100644
index 0000000..3fd76a8
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.sp_RecordResourceGathered.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.sparkAfterSync.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.sparkAfterSync.StoredProcedure.sql
new file mode 100644
index 0000000..c6f025d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.sparkAfterSync.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.sparkPrepareSync.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.sparkPrepareSync.StoredProcedure.sql
new file mode 100644
index 0000000..0a5dc69
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.sparkPrepareSync.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.standingReimburseFromLog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.standingReimburseFromLog.StoredProcedure.sql
new file mode 100644
index 0000000..6b54351
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.standingReimburseFromLog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.standingsCleanUp.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.standingsCleanUp.StoredProcedure.sql
new file mode 100644
index 0000000..5dcae71
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.standingsCleanUp.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.startup_protectedzones.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.startup_protectedzones.StoredProcedure.sql
new file mode 100644
index 0000000..a7830a4
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.startup_protectedzones.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.startup_standalone.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.startup_standalone.StoredProcedure.sql
new file mode 100644
index 0000000..c30f69c
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.startup_standalone.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.teleportFixData.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.teleportFixData.StoredProcedure.sql
new file mode 100644
index 0000000..1319ca1
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.teleportFixData.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.usp_RefreshAutoMarketOrders.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.usp_RefreshAutoMarketOrders.StoredProcedure.sql
new file mode 100644
index 0000000..e0a50a2
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.usp_RefreshAutoMarketOrders.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.vendorPresetDelete.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.vendorPresetDelete.StoredProcedure.sql
new file mode 100644
index 0000000..bad91fa
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.vendorPresetDelete.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.vendorPresetInsertToMarket.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.vendorPresetInsertToMarket.StoredProcedure.sql
new file mode 100644
index 0000000..2a5ea58
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.vendorPresetInsertToMarket.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.vendorPresetSaveFromMarket.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.vendorPresetSaveFromMarket.StoredProcedure.sql
new file mode 100644
index 0000000..997eb31
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.vendorPresetSaveFromMarket.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.writeEnvironment.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.writeEnvironment.StoredProcedure.sql
new file mode 100644
index 0000000..2fa07c6
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.writeEnvironment.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.writeHardwareInfo.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.writeHardwareInfo.StoredProcedure.sql
new file mode 100644
index 0000000..6eaed33
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.writeHardwareInfo.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.writeharvestlog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.writeharvestlog.StoredProcedure.sql
new file mode 100644
index 0000000..62716ff
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.writeharvestlog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.writemininglog.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.writemininglog.StoredProcedure.sql
new file mode 100644
index 0000000..679cb0d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.writemininglog.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/dbo.writemissiontarget.StoredProcedure.sql b/docs/db_structure/stored_procedures/dbo.writemissiontarget.StoredProcedure.sql
new file mode 100644
index 0000000..124a05d
Binary files /dev/null and b/docs/db_structure/stored_procedures/dbo.writemissiontarget.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/opp.artifactRefresh.StoredProcedure.sql b/docs/db_structure/stored_procedures/opp.artifactRefresh.StoredProcedure.sql
new file mode 100644
index 0000000..fa6df37
Binary files /dev/null and b/docs/db_structure/stored_procedures/opp.artifactRefresh.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/opp.characterForceDockHome.StoredProcedure.sql b/docs/db_structure/stored_procedures/opp.characterForceDockHome.StoredProcedure.sql
new file mode 100644
index 0000000..fcd93ec
Binary files /dev/null and b/docs/db_structure/stored_procedures/opp.characterForceDockHome.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/opp.extensionSubscriptionStart.StoredProcedure.sql b/docs/db_structure/stored_procedures/opp.extensionSubscriptionStart.StoredProcedure.sql
new file mode 100644
index 0000000..5e93619
Binary files /dev/null and b/docs/db_structure/stored_procedures/opp.extensionSubscriptionStart.StoredProcedure.sql differ
diff --git a/docs/db_structure/stored_procedures/opp.getExtensionSubscription.StoredProcedure.sql b/docs/db_structure/stored_procedures/opp.getExtensionSubscription.StoredProcedure.sql
new file mode 100644
index 0000000..022f43a
Binary files /dev/null and b/docs/db_structure/stored_procedures/opp.getExtensionSubscription.StoredProcedure.sql differ
diff --git a/docs/db_structure/views/foreignKeys.sql b/docs/db_structure/views/foreignKeys.sql
new file mode 100644
index 0000000..e275c67
--- /dev/null
+++ b/docs/db_structure/views/foreignKeys.sql
@@ -0,0 +1,26 @@
+/****** Object: View [dbo].[foreignKeys] Script Date: 10.05.2026 7:23:51 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE view [dbo].[foreignKeys] as
+ select cast(f.name as varchar(255)) as foreign_key_name
+ , r.keycnt
+ , cast(c.name as varchar(255)) as foreign_table
+ , cast(fc.name as varchar(255)) as foreign_column_1
+ , cast(fc2.name as varchar(255)) as foreign_column_2
+ , cast(p.name as varchar(255)) as primary_table
+ , cast(rc.name as varchar(255)) as primary_column_1
+ , cast(rc2.name as varchar(255)) as primary_column_2
+ from sysobjects f
+ inner join sysobjects c on f.parent_obj = c.id
+ inner join sysreferences r on f.id = r.constid
+ inner join sysobjects p on r.rkeyid = p.id
+ inner join syscolumns rc on r.rkeyid = rc.id and r.rkey1 = rc.colid
+ inner join syscolumns fc on r.fkeyid = fc.id and r.fkey1 = fc.colid
+ left join syscolumns rc2 on r.rkeyid = rc2.id and r.rkey2 = rc.colid
+ left join syscolumns fc2 on r.fkeyid = fc2.id and r.fkey2 = fc.colid
+ where f.type = 'F'
+GO
\ No newline at end of file
diff --git a/docs/db_structure/views/production_data.sql b/docs/db_structure/views/production_data.sql
new file mode 100644
index 0000000..d21fd44
--- /dev/null
+++ b/docs/db_structure/views/production_data.sql
@@ -0,0 +1,21 @@
+/****** Object: View [dbo].[production_data] Script Date: 10.05.2026 7:24:41 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+CREATE VIEW [dbo].[production_data] AS
+SELECT
+ ed.definition AS itemdefinition,
+ ed.definitionname AS product,
+ ced.definitionname AS components,
+ c.componentamount AS amount
+FROM components c
+INNER JOIN entitydefaults ed ON c.definition = ed.definition
+INNER JOIN entitydefaults ced ON c.componentdefinition = ced.definition
+WHERE ed.purchasable = 1 AND ed.enabled = 1 AND ed.hidden = 0;-- AND (ed.tiertype IS NULL OR ed.tiertype = 1);-- AND ed.attributeflags & CONVERT(BIGINT, 2147483648) = 0;
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/views/randomView.sql b/docs/db_structure/views/randomView.sql
new file mode 100644
index 0000000..d96cc93
--- /dev/null
+++ b/docs/db_structure/views/randomView.sql
@@ -0,0 +1,11 @@
+/****** Object: View [dbo].[randomView] Script Date: 10.05.2026 7:25:26 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE VIEW [dbo].[randomView]
+AS
+SELECT RAND() rndResult
+GO
\ No newline at end of file
diff --git a/docs/db_structure/views/v_all_production_costs.sql b/docs/db_structure/views/v_all_production_costs.sql
new file mode 100644
index 0000000..1ebc1f6
--- /dev/null
+++ b/docs/db_structure/views/v_all_production_costs.sql
@@ -0,0 +1,82 @@
+/****** Object: View [dbo].[v_all_production_costs] Script Date: 10.05.2026 7:27:10 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+---- Use both based and calculated values
+
+CREATE VIEW [dbo].[v_all_production_costs] AS
+WITH all_items AS (
+ SELECT product AS item FROM production_data
+ UNION
+ SELECT components AS item FROM production_data
+),
+recursive_materials AS (
+ SELECT
+ base.item,
+ pd.components AS raw_material,
+ CAST(pd.amount * 2.1 AS FLOAT) AS quantity
+ FROM all_items base
+ JOIN production_data pd ON pd.product = base.item
+
+ UNION ALL
+
+ SELECT
+ rm.item,
+ pd.components AS raw_material,
+ rm.quantity * pd.amount * 2.1 AS quantity
+ FROM recursive_materials rm
+ JOIN production_data pd ON rm.raw_material = pd.product
+),
+aggregated_costs AS (
+ SELECT
+ rm.item AS product,
+ rm.raw_material,
+ SUM(rm.quantity) AS total_quantity
+ FROM recursive_materials rm
+ GROUP BY rm.item, rm.raw_material
+),
+latest_market_prices AS (
+ SELECT rmp.resource_name, rmp.unit_price
+ FROM resource_market_prices rmp
+ WHERE rmp.calculated_on = (SELECT MAX(calculated_on) FROM resource_market_prices)
+),
+computed_costs AS (
+ SELECT
+ ac.product,
+ SUM(
+ ac.total_quantity *
+ ISNULL(mp.unit_price, base.price_nic)
+ ) AS production_cost_nic
+ FROM aggregated_costs ac
+ LEFT JOIN latest_market_prices mp
+ ON ac.raw_material COLLATE DATABASE_DEFAULT = mp.resource_name COLLATE DATABASE_DEFAULT
+ LEFT JOIN raw_material_prices base
+ ON ac.raw_material COLLATE DATABASE_DEFAULT = base.material_name COLLATE DATABASE_DEFAULT
+ GROUP BY ac.product
+),
+raw_resources AS (
+ SELECT
+ rmp.material_name AS product,
+ ISNULL(mp.unit_price, rmp.price_nic) AS production_cost_nic
+ FROM raw_material_prices rmp
+ LEFT JOIN latest_market_prices mp
+ ON rmp.material_name COLLATE DATABASE_DEFAULT = mp.resource_name COLLATE DATABASE_DEFAULT
+ WHERE NOT EXISTS (
+ SELECT 1 FROM production_data pd WHERE pd.product = rmp.material_name
+ )
+),
+final_costs AS (
+ SELECT * FROM computed_costs
+ UNION
+ SELECT * FROM raw_resources
+)
+SELECT
+ product,
+ ROUND(production_cost_nic, 2) AS production_cost_nic
+FROM final_costs;
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/views/v_required_raw_materials.sql b/docs/db_structure/views/v_required_raw_materials.sql
new file mode 100644
index 0000000..1c9b044
--- /dev/null
+++ b/docs/db_structure/views/v_required_raw_materials.sql
@@ -0,0 +1,44 @@
+/****** Object: View [dbo].[v_required_raw_materials] Script Date: 10.05.2026 7:26:34 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+
+
+
+-- Create the view
+CREATE VIEW [dbo].[v_required_raw_materials] AS
+ WITH RecursiveBreakdown AS (
+ -- Base case: direct components
+ SELECT
+ moc.definitionname AS product,
+ pd.components AS component,
+ SUM(CAST(ROUND(pd.amount * 2.1, 0) AS BIGINT)) AS total_amount -- 50% efficiency adjustment
+ FROM dbo.market_orders_configuration moc
+ JOIN dbo.production_data pd ON moc.definitionname = pd.product
+ GROUP BY moc.definitionname, pd.components
+
+ UNION ALL
+
+ -- Recursive case: break down intermediate components
+ SELECT
+ rb.product,
+ pd.components AS component,
+ rb.total_amount * CAST(ROUND(pd.amount * 2.1, 0) AS BIGINT) AS total_amount
+ FROM RecursiveBreakdown rb
+ JOIN dbo.production_data pd ON rb.component = pd.product
+ )
+
+ -- Final aggregation: only raw materials (not further craftable)
+ SELECT
+ rb.product as product,
+ rb.component AS raw_material,
+ SUM(rb.total_amount) AS total_quantity
+ FROM RecursiveBreakdown rb
+ LEFT JOIN dbo.production_data pd ON rb.component = pd.product
+ WHERE pd.product IS NULL
+ GROUP BY rb.product, rb.component;
+
+GO
\ No newline at end of file
diff --git a/docs/db_structure/views/view_itemresearchlevels.sql b/docs/db_structure/views/view_itemresearchlevels.sql
new file mode 100644
index 0000000..2a56cb7
--- /dev/null
+++ b/docs/db_structure/views/view_itemresearchlevels.sql
@@ -0,0 +1,125 @@
+/****** Object: View [dbo].[view_itemresearchlevels] Script Date: 10.05.2026 7:27:44 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE VIEW [dbo].[view_itemresearchlevels]
+AS
+SELECT id, definition, dbo.GetDefinitionName(definition) AS defname, calibrationprogram, dbo.GetDefinitionName(calibrationprogram) AS cprgname, enabled
+FROM dbo.itemresearchlevels
+GO
+
+EXEC sys.sp_addextendedproperty @name=N'MS_DiagramPane1', @value=N'[0E232FF0-B466-11cf-A24F-00AA00A3EFFF, 1.00]
+Begin DesignProperties =
+ Begin PaneConfigurations =
+ Begin PaneConfiguration = 0
+ NumPanes = 4
+ Configuration = "(H (1[40] 4[20] 2[20] 3) )"
+ End
+ Begin PaneConfiguration = 1
+ NumPanes = 3
+ Configuration = "(H (1 [50] 4 [25] 3))"
+ End
+ Begin PaneConfiguration = 2
+ NumPanes = 3
+ Configuration = "(H (1 [50] 2 [25] 3))"
+ End
+ Begin PaneConfiguration = 3
+ NumPanes = 3
+ Configuration = "(H (4 [30] 2 [40] 3))"
+ End
+ Begin PaneConfiguration = 4
+ NumPanes = 2
+ Configuration = "(H (1 [56] 3))"
+ End
+ Begin PaneConfiguration = 5
+ NumPanes = 2
+ Configuration = "(H (2 [66] 3))"
+ End
+ Begin PaneConfiguration = 6
+ NumPanes = 2
+ Configuration = "(H (4 [50] 3))"
+ End
+ Begin PaneConfiguration = 7
+ NumPanes = 1
+ Configuration = "(V (3))"
+ End
+ Begin PaneConfiguration = 8
+ NumPanes = 3
+ Configuration = "(H (1[56] 4[18] 2) )"
+ End
+ Begin PaneConfiguration = 9
+ NumPanes = 2
+ Configuration = "(H (1 [75] 4))"
+ End
+ Begin PaneConfiguration = 10
+ NumPanes = 2
+ Configuration = "(H (1[66] 2) )"
+ End
+ Begin PaneConfiguration = 11
+ NumPanes = 2
+ Configuration = "(H (4 [60] 2))"
+ End
+ Begin PaneConfiguration = 12
+ NumPanes = 1
+ Configuration = "(H (1) )"
+ End
+ Begin PaneConfiguration = 13
+ NumPanes = 1
+ Configuration = "(V (4))"
+ End
+ Begin PaneConfiguration = 14
+ NumPanes = 1
+ Configuration = "(V (2))"
+ End
+ ActivePaneConfig = 0
+ End
+ Begin DiagramPane =
+ Begin Origin =
+ Top = 0
+ Left = 0
+ End
+ Begin Tables =
+ Begin Table = "itemresearchlevels"
+ Begin Extent =
+ Top = 6
+ Left = 38
+ Bottom = 346
+ Right = 605
+ End
+ DisplayFlags = 280
+ TopColumn = 0
+ End
+ End
+ End
+ Begin SQLPane =
+ End
+ Begin DataPane =
+ Begin ParameterDefaults = ""
+ End
+ End
+ Begin CriteriaPane =
+ Begin ColumnWidths = 11
+ Column = 1440
+ Alias = 1710
+ Table = 2055
+ Output = 720
+ Append = 1400
+ NewValue = 1170
+ SortType = 1350
+ SortOrder = 1410
+ GroupBy = 1350
+ Filter = 1350
+ Or = 1350
+ Or = 1350
+ Or = 1350
+ End
+ End
+End
+' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'VIEW',@level1name=N'view_itemresearchlevels'
+GO
+
+EXEC sys.sp_addextendedproperty @name=N'MS_DiagramPaneCount', @value=1 , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'VIEW',@level1name=N'view_itemresearchlevels'
+GO
\ No newline at end of file
diff --git a/docs/db_structure/views/view_prototypes.sql b/docs/db_structure/views/view_prototypes.sql
new file mode 100644
index 0000000..c76f758
--- /dev/null
+++ b/docs/db_structure/views/view_prototypes.sql
@@ -0,0 +1,125 @@
+/****** Object: View [dbo].[view_prototypes] Script Date: 10.05.2026 7:28:32 ******/
+SET ANSI_NULLS ON
+GO
+
+SET QUOTED_IDENTIFIER ON
+GO
+
+CREATE VIEW [dbo].[view_prototypes]
+AS
+SELECT id, definition, dbo.GetDefinitionName(definition) AS defname, prototype, dbo.GetDefinitionName(prototype) AS protoname
+FROM dbo.prototypes
+GO
+
+EXEC sys.sp_addextendedproperty @name=N'MS_DiagramPane1', @value=N'[0E232FF0-B466-11cf-A24F-00AA00A3EFFF, 1.00]
+Begin DesignProperties =
+ Begin PaneConfigurations =
+ Begin PaneConfiguration = 0
+ NumPanes = 4
+ Configuration = "(H (1[40] 4[20] 2[20] 3) )"
+ End
+ Begin PaneConfiguration = 1
+ NumPanes = 3
+ Configuration = "(H (1 [50] 4 [25] 3))"
+ End
+ Begin PaneConfiguration = 2
+ NumPanes = 3
+ Configuration = "(H (1 [50] 2 [25] 3))"
+ End
+ Begin PaneConfiguration = 3
+ NumPanes = 3
+ Configuration = "(H (4 [30] 2 [40] 3))"
+ End
+ Begin PaneConfiguration = 4
+ NumPanes = 2
+ Configuration = "(H (1 [56] 3))"
+ End
+ Begin PaneConfiguration = 5
+ NumPanes = 2
+ Configuration = "(H (2 [66] 3))"
+ End
+ Begin PaneConfiguration = 6
+ NumPanes = 2
+ Configuration = "(H (4 [50] 3))"
+ End
+ Begin PaneConfiguration = 7
+ NumPanes = 1
+ Configuration = "(V (3))"
+ End
+ Begin PaneConfiguration = 8
+ NumPanes = 3
+ Configuration = "(H (1[56] 4[18] 2) )"
+ End
+ Begin PaneConfiguration = 9
+ NumPanes = 2
+ Configuration = "(H (1 [75] 4))"
+ End
+ Begin PaneConfiguration = 10
+ NumPanes = 2
+ Configuration = "(H (1[66] 2) )"
+ End
+ Begin PaneConfiguration = 11
+ NumPanes = 2
+ Configuration = "(H (4 [60] 2))"
+ End
+ Begin PaneConfiguration = 12
+ NumPanes = 1
+ Configuration = "(H (1) )"
+ End
+ Begin PaneConfiguration = 13
+ NumPanes = 1
+ Configuration = "(V (4))"
+ End
+ Begin PaneConfiguration = 14
+ NumPanes = 1
+ Configuration = "(V (2))"
+ End
+ ActivePaneConfig = 0
+ End
+ Begin DiagramPane =
+ Begin Origin =
+ Top = 0
+ Left = 0
+ End
+ Begin Tables =
+ Begin Table = "prototypes"
+ Begin Extent =
+ Top = 6
+ Left = 38
+ Bottom = 99
+ Right = 189
+ End
+ DisplayFlags = 280
+ TopColumn = 0
+ End
+ End
+ End
+ Begin SQLPane =
+ End
+ Begin DataPane =
+ Begin ParameterDefaults = ""
+ End
+ End
+ Begin CriteriaPane =
+ Begin ColumnWidths = 11
+ Column = 1440
+ Alias = 900
+ Table = 1170
+ Output = 720
+ Append = 1400
+ NewValue = 1170
+ SortType = 1350
+ SortOrder = 1410
+ GroupBy = 1350
+ Filter = 1350
+ Or = 1350
+ Or = 1350
+ Or = 1350
+ End
+ End
+End
+' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'VIEW',@level1name=N'view_prototypes'
+GO
+
+EXEC sys.sp_addextendedproperty @name=N'MS_DiagramPaneCount', @value=1 , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'VIEW',@level1name=N'view_prototypes'
+GO
\ No newline at end of file
diff --git a/docs/superpowers/plans/2026-05-10-season-intro-mail-improvements.md b/docs/superpowers/plans/2026-05-10-season-intro-mail-improvements.md
new file mode 100644
index 0000000..57ef089
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-10-season-intro-mail-improvements.md
@@ -0,0 +1,373 @@
+# Season Intro Mail Improvements Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Fix three gaps in the Season intro-mail flow: online players miss the mail at activation time; docked-but-logged-in players miss the mail entirely; and the email body contains no useful content about the season.
+
+**Architecture:** All changes live in `SeasonService` (the single authoritative mail sender) and the single hook site in `Player.cs`. Task 1 fixes the activation path. Task 2 moves the login hook from zone-entry to character-selection so docked players are covered. Task 3 rewrites `SendIntroMail` with a rich body and injects `ICustomDictionary` for item-name translation.
+
+**Tech Stack:** .NET 8 / C#, Autofac DI, `ISessionManager` session events, `ICustomDictionary`, `EntityDefault.Reader`.
+
+---
+
+## File Structure
+
+| File | Change |
+|---|---|
+| `src/Perpetuum/Services/Seasons/SeasonService.cs` | All three tasks: fix activation path, wire session events, rich email body |
+| `src/Perpetuum/Players/Player.cs` | Task 2 only: remove `OnEnterZone` hook |
+
+`SeasonModule.cs` needs no changes — Autofac resolves `ICustomDictionary` from the constructor automatically.
+
+---
+
+## Task 1: Fix activation mail for online players
+
+**Files:**
+- Modify: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+
+The bug: `SendActivationMailToOnlineCharacters` sends mail without calling `RefreshCache()` first
+(so `_activeRates`/`_activeTiers`/`_activeObjectives` are empty stale snapshots) and without calling
+`TryMarkIntroMailSent` (so online players receive a duplicate on their next reconnect).
+
+- [ ] **Step 1: Replace `SendActivationMailToOnlineCharacters`**
+
+Find this method (currently at the bottom of `SeasonService.cs`):
+
+```csharp
+public void SendActivationMailToOnlineCharacters(Season season)
+{
+ foreach (var character in _sessionManager.SelectedCharacters)
+ {
+ if (character == null || character == Character.None)
+ continue;
+
+ SendIntroMail(character, season);
+ }
+}
+```
+
+Replace it entirely with:
+
+```csharp
+public void SendActivationMailToOnlineCharacters(Season season)
+{
+ RefreshCache();
+ var freshSeason = _activeSeason;
+ if (freshSeason == null) return;
+
+ foreach (var character in _sessionManager.SelectedCharacters)
+ {
+ if (character == null || character == Character.None)
+ continue;
+
+ if (_repository.TryMarkIntroMailSent(character.Id, freshSeason.Id))
+ SendIntroMail(character, freshSeason);
+ }
+}
+```
+
+- [ ] **Step 2: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonService.cs
+git commit -m "fix(seasons): refresh cache and mark intro-mail flag before sending to online players at activation"
+```
+
+---
+
+## Task 2: Move login hook from zone-entry to character-selection
+
+**Files:**
+- Modify: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+- Modify: `src/Perpetuum/Players/Player.cs`
+
+The bug: `OnCharacterLogin` is hooked in `Player.OnEnterZone`, which only fires when a player
+deploys into a zone. Players who log in and stay docked in a terminal never trigger it.
+
+The fix: subscribe to `ISessionManager.SessionAdded` in the `SeasonService` constructor, and for
+each session subscribe to `session.CharacterSelected`. `CharacterSelected` fires as soon as a
+character is picked from the character screen — before any zone is entered.
+
+`SessionEventHandler` signature: `delegate void SessionEventHandler(ISession session)`
+`SessionEventHandler` signature: `delegate void SessionEventHandler(ISession session, T args)`
+
+Both events are already declared on `ISessionManager` and `ISession`.
+
+- [ ] **Step 1: Add `OnSessionAdded` private method to `SeasonService`**
+
+Insert this method anywhere in the private section of `SeasonService` (e.g., after the constructor):
+
+```csharp
+private void OnSessionAdded(ISession session)
+{
+ session.CharacterSelected += (_, character) => OnCharacterLogin(character);
+}
+```
+
+- [ ] **Step 2: Wire `SessionAdded` in the `SeasonService` constructor**
+
+The constructor currently reads:
+
+```csharp
+public SeasonService(SeasonRepository repository, ISessionManager sessionManager)
+{
+ _repository = repository;
+ _sessionManager = sessionManager;
+}
+```
+
+Add the event subscription at the end of the constructor body (no DB calls involved):
+
+```csharp
+public SeasonService(SeasonRepository repository, ISessionManager sessionManager)
+{
+ _repository = repository;
+ _sessionManager = sessionManager;
+ _sessionManager.SessionAdded += OnSessionAdded;
+}
+```
+
+You also need to add `using Perpetuum.Services.Sessions;` if it is not already present at the top
+of the file. Check existing usings — it is already there (`using Perpetuum.Services.Sessions;` on
+line 6), so no change needed.
+
+- [ ] **Step 3: Remove the `OnEnterZone` hook from `Player.cs`**
+
+Open `src/Perpetuum/Players/Player.cs`. Find (around line 922):
+
+```csharp
+protected override void OnEnterZone(IZone zone, ZoneEnterType enterType)
+{
+ base.OnEnterZone(zone, enterType);
+ SeasonServiceLocator.Instance?.OnCharacterLogin(Character);
+ check = PlayerMoveCheckQueue.Create(this, CurrentPosition);
+```
+
+Remove the season hook line so it reads:
+
+```csharp
+protected override void OnEnterZone(IZone zone, ZoneEnterType enterType)
+{
+ base.OnEnterZone(zone, enterType);
+ check = PlayerMoveCheckQueue.Create(this, CurrentPosition);
+```
+
+- [ ] **Step 4: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonService.cs
+git add src/Perpetuum/Players/Player.cs
+git commit -m "fix(seasons): move intro-mail hook from OnEnterZone to CharacterSelected so docked players are covered"
+```
+
+---
+
+## Task 3: Rich intro email body with objectives, tiers, and reward items
+
+**Files:**
+- Modify: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+
+Adds `ICustomDictionary` constructor injection, a `Translate` helper, an `ActivityTypeName` helper,
+and rewrites `SendIntroMail` to include scoring rates, objectives, and tier rewards with translated
+item names and quantities.
+
+`EntityDefault.Reader.Get(int definition)` returns an `EntityDefault` whose `.Name` property is the
+dictionary key (e.g. `"def_syndicate_novice_license"`). Look up that key in the English dictionary
+(language `0`) to get the display name. Fall back to the key itself if missing.
+
+- [ ] **Step 1: Add `using` directives and the `_customDictionary` field**
+
+At the top of `SeasonService.cs`, the current usings are:
+
+```csharp
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using Perpetuum.Accounting.Characters;
+using Perpetuum.Services.Mail;
+using Perpetuum.Services.Sessions;
+using Perpetuum.Threading.Process;
+```
+
+Replace with (adds `System.Collections.Generic`, `System.Text`, and `Perpetuum.EntityFramework`):
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Perpetuum.Accounting.Characters;
+using Perpetuum.EntityFramework;
+using Perpetuum.Services.Mail;
+using Perpetuum.Services.Sessions;
+using Perpetuum.Threading.Process;
+```
+
+Add the field alongside the other readonly fields (after `_sessionManager`):
+
+```csharp
+private readonly ISessionManager _sessionManager;
+private readonly ICustomDictionary _customDictionary;
+private readonly Lazy _announcer = new(() => Character.GetByNick(AnnouncerNick));
+```
+
+- [ ] **Step 2: Update the constructor signature to inject `ICustomDictionary`**
+
+Change the constructor from:
+
+```csharp
+public SeasonService(SeasonRepository repository, ISessionManager sessionManager)
+{
+ _repository = repository;
+ _sessionManager = sessionManager;
+ _sessionManager.SessionAdded += OnSessionAdded;
+}
+```
+
+To:
+
+```csharp
+public SeasonService(SeasonRepository repository, ISessionManager sessionManager,
+ ICustomDictionary customDictionary)
+{
+ _repository = repository;
+ _sessionManager = sessionManager;
+ _customDictionary = customDictionary;
+ _sessionManager.SessionAdded += OnSessionAdded;
+}
+```
+
+Autofac resolves `ICustomDictionary` automatically — `CustomDictionary` is already registered as
+`ICustomDictionary` singleton in `PerpetuumBootstrapper.InitContainer` via:
+```csharp
+_builder.RegisterType().As().SingleInstance().AutoActivate();
+```
+No module change needed.
+
+- [ ] **Step 3: Add `Translate` and `ActivityTypeName` helpers**
+
+Add these two private static methods anywhere in the private section of `SeasonService`:
+
+```csharp
+private static string Translate(string key, Dictionary? dict)
+{
+ if (dict != null && dict.TryGetValue(key, out var val) && val is string s && s.Length > 0)
+ return s;
+ return key;
+}
+
+private static string ActivityTypeName(SeasonActivityType type) => type switch
+{
+ SeasonActivityType.NpcKill => "NPC Kill",
+ SeasonActivityType.PvpKill => "PvP Kill",
+ SeasonActivityType.MissionComplete => "Mission Completed",
+ SeasonActivityType.MineralMined => "Mineral Mined",
+ SeasonActivityType.EpSpent => "EP Spent",
+ SeasonActivityType.NicEarned => "NIC Earned",
+ SeasonActivityType.NicSpent => "NIC Spent",
+ SeasonActivityType.IntrusionPoint => "Intrusion SAP",
+ _ => type.ToString(),
+};
+```
+
+- [ ] **Step 4: Rewrite `SendIntroMail`**
+
+Find the current `SendIntroMail`:
+
+```csharp
+private void SendIntroMail(Character character, Season season)
+{
+ string subject = $"Season Active: {season.Name}";
+ string body = $"{season.Description}\n\nSeason ends: {season.EndTime:yyyy-MM-dd HH:mm} UTC";
+ MailHandler.SendMail(_announcer.Value, character, subject, body,
+ MailType.character, out _, out _);
+}
+```
+
+Replace it with:
+
+```csharp
+private void SendIntroMail(Character character, Season season)
+{
+ var dict = _customDictionary.GetDictionary(0);
+ var sb = new StringBuilder();
+
+ if (!string.IsNullOrWhiteSpace(season.Description))
+ sb.AppendLine(season.Description).AppendLine();
+
+ sb.AppendLine($"Season ends: {season.EndTime:yyyy-MM-dd HH:mm} UTC");
+
+ var rates = _activeRates;
+ if (rates.Count > 0)
+ {
+ sb.AppendLine().AppendLine("-- Scoring --");
+ foreach (var rate in rates)
+ {
+ string unitDesc = rate.UnitScale > 1 ? $" per {rate.UnitScale:N0}" : "";
+ sb.AppendLine($" {ActivityTypeName(rate.ActivityType)}: {rate.PointsPerUnit:G} pts{unitDesc}");
+ }
+ }
+
+ var objectives = _activeObjectives;
+ if (objectives.Count > 0)
+ {
+ sb.AppendLine().AppendLine("-- Objectives --");
+ foreach (var obj in objectives.OrderBy(o => o.DisplayOrder))
+ sb.AppendLine($" {obj.Name}: reach {obj.TargetValue:N0} {ActivityTypeName(obj.ActivityType)} → +{obj.BonusPoints} pts bonus");
+ }
+
+ var tiers = _activeTiers;
+ if (tiers.Count > 0)
+ {
+ sb.AppendLine().AppendLine("-- Tier Rewards --");
+ foreach (var tier in tiers)
+ {
+ sb.AppendLine($" {tier.TierName} ({tier.PointsRequired:N0} pts):");
+ foreach (var item in _repository.GetPackageItems(tier.PackageId))
+ {
+ var ed = EntityDefault.Reader.Get(item.Definition);
+ string name = (ed != null && ed != EntityDefault.None)
+ ? Translate(ed.Name, dict)
+ : item.Definition.ToString();
+ sb.AppendLine($" - {name} x{item.Quantity}");
+ }
+ }
+ }
+
+ MailHandler.SendMail(_announcer.Value, character, $"Season Active: {season.Name}",
+ sb.ToString(), MailType.character, out _, out _);
+}
+```
+
+- [ ] **Step 5: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonService.cs
+git commit -m "feat(seasons): enrich intro email with scoring rates, objectives, and tier reward items with translated names"
+```
diff --git a/docs/superpowers/plans/2026-05-10-season-start-notification.md b/docs/superpowers/plans/2026-05-10-season-start-notification.md
new file mode 100644
index 0000000..70d8b17
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-10-season-start-notification.md
@@ -0,0 +1,223 @@
+# Season Start Notification Fix Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make `RefreshCache()` the single point that detects a new active season and immediately notifies all currently-online players via the season intro email.
+
+**Architecture:** Add a `_lastNotifiedSeasonId` nullable-int field to `SeasonService`. In `RefreshCache()`, after loading the active season, compare its ID to `_lastNotifiedSeasonId`; on mismatch, iterate `_sessionManager.SelectedCharacters`, call `TryMarkIntroMailSent`+`SendIntroMail` per character, then update `_lastNotifiedSeasonId`. Simplify `SendActivationMailToOnlineCharacters` to just call `RefreshCache()` — the admin command path stays eager, the Update-loop path works as a fallback. The DB-level `TryMarkIntroMailSent` guard prevents double-delivery under concurrent calls.
+
+**Tech Stack:** .NET 8 / C#, Autofac DI, SQL Server via `Db.Query`.
+
+> **Note:** This repo has no automated test suite. Verification is by build success and manual server run.
+
+---
+
+## File Structure
+
+| File | Change |
+|---|---|
+| `src/Perpetuum/Services/Seasons/SeasonService.cs` | Add `_lastNotifiedSeasonId` field; extract `NotifyOnlinePlayersSeasonStarted()` helper; modify `RefreshCache()` to call it on transition; simplify `SendActivationMailToOnlineCharacters` |
+
+---
+
+## Task 1: Add `_lastNotifiedSeasonId` field and notification helper
+
+**Files:**
+- Modify: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+
+- [ ] **Step 1: Add `_lastNotifiedSeasonId` field**
+
+In `SeasonService.cs`, the volatile fields block (currently lines 26–30) reads:
+
+```csharp
+// Replaced atomically on refresh — reads are always against a stable snapshot.
+private volatile Season? _activeSeason;
+private ImmutableList _activeRates = ImmutableList.Empty;
+private ImmutableList _activeObjectives = ImmutableList.Empty;
+private ImmutableList _activeTiers = ImmutableList.Empty;
+private ImmutableList _activeLeaderboard = ImmutableList.Empty;
+
+// Trigger immediate load on first Update tick
+private TimeSpan _cacheAge = CacheRefreshInterval;
+```
+
+Replace it with:
+
+```csharp
+// Replaced atomically on refresh — reads are always against a stable snapshot.
+private volatile Season? _activeSeason;
+private ImmutableList _activeRates = ImmutableList.Empty;
+private ImmutableList _activeObjectives = ImmutableList.Empty;
+private ImmutableList _activeTiers = ImmutableList.Empty;
+private ImmutableList _activeLeaderboard = ImmutableList.Empty;
+
+// Tracks which season we have already dispatched intro mail for.
+private int? _lastNotifiedSeasonId;
+
+// Trigger immediate load on first Update tick
+private TimeSpan _cacheAge = CacheRefreshInterval;
+```
+
+- [ ] **Step 2: Add `NotifyOnlinePlayersSeasonStarted` private helper**
+
+In the `// ── Mail helpers ─────` section (after `SendActivationMailToOnlineCharacters`, before `SendIntroMail`), add:
+
+```csharp
+private void NotifyOnlinePlayersSeasonStarted(Season season)
+{
+ foreach (var character in _sessionManager.SelectedCharacters)
+ {
+ if (character == null || character == Character.None)
+ continue;
+
+ if (_repository.TryMarkIntroMailSent(character.Id, season.Id))
+ SendIntroMail(character, season);
+ }
+}
+```
+
+- [ ] **Step 3: Build to verify no errors**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: `Build succeeded. 0 Error(s)`
+
+---
+
+## Task 2: Wire notification into `RefreshCache()`
+
+**Files:**
+- Modify: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+
+- [ ] **Step 1: Modify `RefreshCache()` to detect season-start transition**
+
+The current `RefreshCache()` body (lines 66–83) reads:
+
+```csharp
+internal void RefreshCache()
+{
+ var season = _repository.GetActiveSeason();
+ if (season == null)
+ {
+ _activeSeason = null;
+ _activeRates = ImmutableList.Empty;
+ _activeObjectives = ImmutableList.Empty;
+ _activeTiers = ImmutableList.Empty;
+ _activeLeaderboard = ImmutableList.Empty;
+ return;
+ }
+
+ _activeRates = _repository.GetActivityRates(season.Id).ToImmutableList();
+ _activeObjectives = _repository.GetObjectives(season.Id).ToImmutableList();
+ _activeTiers = _repository.GetTiers(season.Id).ToImmutableList();
+ _activeLeaderboard = _repository.GetLeaderboardRewards(season.Id).ToImmutableList();
+ _activeSeason = season; // assign last so readers see a consistent snapshot
+}
+```
+
+Replace it with:
+
+```csharp
+internal void RefreshCache()
+{
+ var season = _repository.GetActiveSeason();
+ if (season == null)
+ {
+ _activeSeason = null;
+ _activeRates = ImmutableList.Empty;
+ _activeObjectives = ImmutableList.Empty;
+ _activeTiers = ImmutableList.Empty;
+ _activeLeaderboard = ImmutableList.Empty;
+ return;
+ }
+
+ _activeRates = _repository.GetActivityRates(season.Id).ToImmutableList();
+ _activeObjectives = _repository.GetObjectives(season.Id).ToImmutableList();
+ _activeTiers = _repository.GetTiers(season.Id).ToImmutableList();
+ _activeLeaderboard = _repository.GetLeaderboardRewards(season.Id).ToImmutableList();
+ _activeSeason = season; // assign last so readers see a consistent snapshot
+
+ if (_lastNotifiedSeasonId != season.Id)
+ {
+ _lastNotifiedSeasonId = season.Id;
+ NotifyOnlinePlayersSeasonStarted(season);
+ }
+}
+```
+
+- [ ] **Step 2: Build to verify no errors**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: `Build succeeded. 0 Error(s)`
+
+---
+
+## Task 3: Simplify `SendActivationMailToOnlineCharacters`
+
+**Files:**
+- Modify: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+
+- [ ] **Step 1: Collapse the method body to a single `RefreshCache()` call**
+
+The current method (lines 286–300) reads:
+
+```csharp
+public void SendActivationMailToOnlineCharacters(Season season)
+{
+ RefreshCache();
+ var freshSeason = _activeSeason;
+ if (freshSeason == null) return;
+
+ foreach (var character in _sessionManager.SelectedCharacters)
+ {
+ if (character == null || character == Character.None)
+ continue;
+
+ if (_repository.TryMarkIntroMailSent(character.Id, freshSeason.Id))
+ SendIntroMail(character, freshSeason);
+ }
+}
+```
+
+Replace with:
+
+```csharp
+public void SendActivationMailToOnlineCharacters(Season season)
+{
+ RefreshCache();
+}
+```
+
+`RefreshCache()` now detects the new season ID and calls `NotifyOnlinePlayersSeasonStarted` automatically. The `season` parameter is kept in the signature to avoid changing the call site in `SeasonAdminCommandHandlers.SeasonActivate`.
+
+- [ ] **Step 2: Build to verify no errors**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: `Build succeeded. 0 Error(s)`
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonService.cs
+git commit -m "fix(seasons): notify online players in RefreshCache on season start transition"
+```
+
+---
+
+## Verification
+
+Manual test checklist (requires a running server with DB):
+
+1. Start server, log in with a character — confirm no season is active.
+2. Issue `#SeasonActivate,` in admin chat.
+3. Check the character's in-game mailbox — confirm the season intro email arrived immediately.
+4. Log out and back in — confirm no second intro email (idempotent).
+5. Issue `#SeasonActivate,` a second time — confirm no duplicate email.
diff --git a/docs/superpowers/plans/2026-05-10-seasons-admin-tool.md b/docs/superpowers/plans/2026-05-10-seasons-admin-tool.md
new file mode 100644
index 0000000..7b6559c
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-10-seasons-admin-tool.md
@@ -0,0 +1,3696 @@
+# Seasons Admin Tool Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a Seasons tab to Perpetuum.AdminTool for creating, managing, and monitoring game seasons and reward packages.
+
+**Architecture:** Data layer (row models + repositories + change objects) built first; ViewModels built second; WPF Views built last. Each layer builds on the previous. LookupCache gets a small additive change for the `hidden` column.
+
+**Tech Stack:** WPF .NET 8, CommunityToolkit.Mvvm, Microsoft.Data.SqlClient (direct SQL), existing ChangeQueue/IPendingChange pattern.
+
+**Verification command (every task):**
+```
+dotnet build E:\MyStuff\Projects\PerpetuumServer2\PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+---
+
+## Task 1: Add `hidden` column to LookupCache + EntityPickItem
+
+**Goal:** Surface the `entitydefaults.hidden` column on `EntityPickItem` so the package item picker can exclude hidden definitions.
+
+- [ ] Edit `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Common\EntityPickItem.cs` — replace contents with:
+
+```csharp
+namespace Perpetuum.AdminTool.Common
+{
+ public class EntityPickItem
+ {
+ public int Definition { get; init; }
+ public string Name { get; init; } = "";
+
+ // Exposed so consumers (structured editors, NPC-loot/relations dropdowns,
+ // potential category filters) can match on category without an extra DB hit.
+ public long CategoryFlags { get; init; }
+
+ // Mirrors entitydefaults.enabled. Consumers (structured editors, future
+ // selectors) hide disabled rows. Newly inserted rows default to enabled = 1
+ // so they show up automatically once the cache refreshes post-commit.
+ public bool Enabled { get; init; }
+
+ // Mirrors entitydefaults.hidden. The package-item picker uses this to
+ // exclude hidden rows from selection. NULL/missing in DB → false.
+ public bool Hidden { get; init; }
+
+ public string Display => $"{Definition} — {Name}";
+ }
+}
+```
+
+- [ ] Edit `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Common\LookupCache.cs` — change the SQL on line 50 and the reader block. The new file content is:
+
+```csharp
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using Microsoft.Data.SqlClient;
+using Perpetuum.AdminTool.Settings;
+
+namespace Perpetuum.AdminTool.Common
+{
+ ///
+ /// Process-wide cache of small lookup tables that drive dropdowns in multiple tabs:
+ /// entitydefaults (definition, definitionname) and robottemplates (id, name).
+ /// Refresh on app start, after every successful Direct-DB commit, and from the
+ /// per-tab Reload buttons.
+ ///
+ public class LookupCache
+ {
+ public ObservableCollection Entities { get; } = new();
+ public ObservableCollection Templates { get; } = new();
+
+ public Dictionary EntityNamesByDefinition { get; private set; } = new();
+ public Dictionary TemplateNamesById { get; private set; } = new();
+
+ public async Task RefreshEntitiesAsync(ConnectionSettings connection)
+ {
+ await using var cn = new SqlConnection(connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await RefreshEntitiesAsync(cn);
+ }
+
+ public async Task RefreshTemplatesAsync(ConnectionSettings connection)
+ {
+ await using var cn = new SqlConnection(connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await RefreshTemplatesAsync(cn);
+ }
+
+ public async Task RefreshAllAsync(ConnectionSettings connection)
+ {
+ await using var cn = new SqlConnection(connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await RefreshEntitiesAsync(cn);
+ await RefreshTemplatesAsync(cn);
+ }
+
+ private async Task RefreshEntitiesAsync(SqlConnection cn)
+ {
+ var fresh = new List();
+ var names = new Dictionary();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText = "select definition, definitionname, categoryflags, enabled, hidden from entitydefaults order by definitionname";
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ var def = reader.GetInt32(0);
+ var name = reader.IsDBNull(1) ? "" : reader.GetString(1);
+ var categoryFlags = reader.IsDBNull(2) ? 0L : reader.GetInt64(2);
+ var enabled = !reader.IsDBNull(3) && reader.GetBoolean(3);
+ var hidden = !reader.IsDBNull(4) && reader.GetBoolean(4);
+ fresh.Add(new EntityPickItem
+ {
+ Definition = def,
+ Name = name,
+ CategoryFlags = categoryFlags,
+ Enabled = enabled,
+ Hidden = hidden
+ });
+ names[def] = name;
+ }
+ Entities.Clear();
+ foreach (var p in fresh) Entities.Add(p);
+ EntityNamesByDefinition = names;
+ }
+
+ private async Task RefreshTemplatesAsync(SqlConnection cn)
+ {
+ var fresh = new List();
+ var names = new Dictionary();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText = "select id, name from robottemplates order by name";
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ var id = reader.GetInt32(0);
+ var name = reader.IsDBNull(1) ? "" : reader.GetString(1);
+ fresh.Add(new TemplatePickItem { Id = id, Name = name });
+ names[id] = name;
+ }
+ Templates.Clear();
+ foreach (var p in fresh) Templates.Add(p);
+ TemplateNamesById = names;
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 2: PackageItemPickItem with category filter
+
+**Goal:** Provide a filtered pick list for the Package Items entity picker. Filters by enabled, not hidden, and one of 11 root category flags (or any descendant).
+
+- [ ] Create new directory `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Packages\` (no action needed — Write tool creates it).
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Packages\PackageItemPickItem.cs`:
+
+```csharp
+using System.Collections.Generic;
+using System.Linq;
+using Perpetuum.AdminTool.Common;
+using Perpetuum.AdminTool.Entities;
+using Perpetuum.ExportedTypes;
+
+namespace Perpetuum.AdminTool.Packages
+{
+ ///
+ /// Display row for the package-item entity picker. Wraps the chosen definition
+ /// and resolved display name. The picker pre-filters the LookupCache once per
+ /// load (and once per cache refresh) using .
+ ///
+ public record PackageItemPickItem(int Definition, string DisplayName)
+ {
+ public string Display => $"{Definition} — {DisplayName}";
+
+ // The 11 allowed root category flag names. An entity passes the filter if its
+ // categoryflags falls under one of these roots (root match OR descendant match).
+ // See spec §"Entity Picker for Package Items" → Filtering.
+ private static readonly long[] AllowedRoots =
+ {
+ (long)CategoryFlags.cf_robots,
+ (long)CategoryFlags.cf_ammo,
+ (long)CategoryFlags.cf_robot_equipment,
+ (long)CategoryFlags.cf_material,
+ (long)CategoryFlags.cf_production_items,
+ (long)CategoryFlags.cf_gift_packages,
+ (long)CategoryFlags.cf_consumable_items,
+ (long)CategoryFlags.cf_consumable_boosters,
+ (long)CategoryFlags.cf_field_accessories,
+ (long)CategoryFlags.cf_pbs_capsules,
+ (long)CategoryFlags.cf_redeemables,
+ };
+
+ ///
+ /// Builds the allowed list from a LookupCache snapshot. Filters out disabled
+ /// and hidden rows, then accepts only entities whose CategoryFlags fall under
+ /// one of the 11 allowed root categories (descendant match via byte-mask).
+ ///
+ /// `hierarchy` is currently unused but is accepted for forward compatibility
+ /// — a future schema may change category math; passing the precomputed
+ /// hierarchy avoids re-walking the catalog if needed.
+ ///
+ public static List BuildFilteredList(
+ IEnumerable all,
+ CategoryFlagsHierarchy? hierarchy = null)
+ {
+ var result = new List();
+ foreach (var e in all)
+ {
+ if (!e.Enabled) continue;
+ if (e.Hidden) continue;
+ if (e.CategoryFlags == 0) continue;
+ if (!MatchesAnyRoot(e.CategoryFlags)) continue;
+ result.Add(new PackageItemPickItem(e.Definition, e.Name));
+ }
+ return result.OrderBy(p => p.DisplayName, System.StringComparer.OrdinalIgnoreCase).ToList();
+ }
+
+ private static bool MatchesAnyRoot(long entityFlags)
+ {
+ foreach (var root in AllowedRoots)
+ {
+ var mask = CategoryFlagsMask(root);
+ if ((entityFlags & mask) == root) return true;
+ }
+ return false;
+ }
+
+ // Mirror of Perpetuum.CategoryFlagsExtensions.GetCategoryFlagsMask, adapted
+ // to operate on long. Same math used in RobotTemplateSlotViewModel.RebuildAmmoPicks.
+ private static long CategoryFlagsMask(long target)
+ {
+ var mask = unchecked((long)0xFFFFFFFFFFFFFFFFUL);
+ while (((ulong)target & (ulong)mask) > 0)
+ {
+ mask <<= 8;
+ }
+ return ~mask;
+ }
+ }
+}
+```
+
+> **Reference for hierarchy parameter:** The `CategoryFlagsHierarchy` argument is reserved for callers that already hold a built hierarchy and want to pass it through. The current implementation uses pure bit-math (matching `RobotTemplateSlotViewModel.RebuildAmmoPicks`) and does not need the tree.
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 3: Row models (8 files)
+
+**Goal:** Define the row model classes that view-models will operate on.
+
+### Task 3a: SeasonRow
+
+- [ ] Create new directory `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\`.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonRow.cs`:
+
+```csharp
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ public partial class SeasonRow : ObservableObject
+ {
+ // PK is the identity column `id`. 0 means a new (unsaved) row.
+ public int Id { get; }
+
+ public bool IsNew { get; set; }
+ public SeasonSnapshot Original { get; private set; }
+
+ [ObservableProperty] private string _name = "";
+ [ObservableProperty] private string _description = "";
+ [ObservableProperty] private DateTime _startTime;
+ [ObservableProperty] private DateTime _endTime;
+ [ObservableProperty] private bool _isActive;
+
+ public SeasonRow(SeasonSnapshot snapshot)
+ {
+ Id = snapshot.Id;
+ Original = snapshot;
+ ApplySnapshot(snapshot);
+ }
+
+ public void ApplySnapshot(SeasonSnapshot s)
+ {
+ Original = s;
+ Name = s.Name;
+ Description = s.Description;
+ StartTime = s.StartTime;
+ EndTime = s.EndTime;
+ IsActive = s.IsActive;
+ }
+
+ public void RefreshOriginalFromCurrent()
+ {
+ Original = new SeasonSnapshot
+ {
+ Id = Id,
+ Name = Name,
+ Description = Description,
+ StartTime = StartTime,
+ EndTime = EndTime,
+ IsActive = IsActive
+ };
+ }
+
+ public static SeasonRow CreateNew(SeasonSnapshot seed)
+ {
+ return new SeasonRow(seed) { IsNew = true };
+ }
+
+ // Card visual state per spec §Tab Structure → Seasons View → Season Cards.
+ public SeasonCardState CardState
+ {
+ get
+ {
+ if (IsActive) return SeasonCardState.Active;
+ return EndTime > DateTime.UtcNow ? SeasonCardState.Draft : SeasonCardState.Ended;
+ }
+ }
+ }
+
+ public class SeasonSnapshot
+ {
+ public int Id { get; init; }
+ public string Name { get; init; } = "";
+ public string Description { get; init; } = "";
+ public DateTime StartTime { get; init; }
+ public DateTime EndTime { get; init; }
+ public bool IsActive { get; init; }
+ }
+
+ public enum SeasonCardState
+ {
+ Active,
+ Draft,
+ Ended
+ }
+}
+```
+
+### Task 3b: SeasonActivityRateRow
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonActivityRateRow.cs`:
+
+```csharp
+using System.Globalization;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Perpetuum.Services.Seasons;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ public partial class SeasonActivityRateRow : ObservableObject
+ {
+ // PK is identity `id`. 0 = no DB row exists yet for this (season, activity) pair;
+ // the upsert MERGE will INSERT in that case.
+ public int Id { get; set; }
+ public int SeasonId { get; set; }
+
+ [ObservableProperty] private SeasonActivityType _activityType;
+ [ObservableProperty] private double _pointsPerUnit;
+ [ObservableProperty] private int _unitScale = 1;
+
+ public string ActivityTypeLabel => ActivityType switch
+ {
+ SeasonActivityType.NpcKill => "NPC Kill",
+ SeasonActivityType.PvpKill => "PvP Kill",
+ SeasonActivityType.MissionComplete => "Mission Complete",
+ SeasonActivityType.MineralMined => "Mineral Mined",
+ SeasonActivityType.EpSpent => "EP Spent",
+ SeasonActivityType.NicEarned => "NIC Earned",
+ SeasonActivityType.NicSpent => "NIC Spent",
+ SeasonActivityType.IntrusionPoint => "Intrusion Point",
+ _ => ActivityType.ToString()
+ };
+
+ public string EffectiveRate => GetEffectiveRateLabel(ActivityType, PointsPerUnit, UnitScale);
+
+ partial void OnPointsPerUnitChanged(double value) => OnPropertyChanged(nameof(EffectiveRate));
+ partial void OnUnitScaleChanged(int value) => OnPropertyChanged(nameof(EffectiveRate));
+ partial void OnActivityTypeChanged(SeasonActivityType value) => OnPropertyChanged(nameof(EffectiveRate));
+
+ public static string GetEffectiveRateLabel(SeasonActivityType type, double pointsPerUnit, int unitScale)
+ {
+ if (pointsPerUnit == 0) return "Disabled";
+
+ var pts = pointsPerUnit.ToString("0.##", CultureInfo.InvariantCulture);
+ var scale = unitScale.ToString("N0", CultureInfo.InvariantCulture);
+
+ return type switch
+ {
+ SeasonActivityType.NpcKill => $"{pts} pts per kill",
+ SeasonActivityType.PvpKill => $"{pts} pts per kill",
+ SeasonActivityType.MissionComplete => $"{pts} pts per completion",
+ SeasonActivityType.IntrusionPoint => $"{pts} pts per intrusion point",
+ SeasonActivityType.MineralMined => unitScale > 1
+ ? $"{pts} pts per {scale} units mined"
+ : $"{pts} pts per unit mined",
+ SeasonActivityType.EpSpent => unitScale > 1
+ ? $"{pts} pts per {scale} EP spent"
+ : $"{pts} pts per EP spent",
+ SeasonActivityType.NicEarned => unitScale > 1
+ ? $"{pts} pts per {scale} NIC earned"
+ : $"{pts} pts per NIC earned",
+ SeasonActivityType.NicSpent => unitScale > 1
+ ? $"{pts} pts per {scale} NIC spent"
+ : $"{pts} pts per NIC spent",
+ _ => $"{pts} pts"
+ };
+ }
+ }
+}
+```
+
+### Task 3c: SeasonObjectiveRow
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonObjectiveRow.cs`:
+
+```csharp
+using CommunityToolkit.Mvvm.ComponentModel;
+using Perpetuum.Services.Seasons;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ public partial class SeasonObjectiveRow : ObservableObject
+ {
+ public int Id { get; set; } // 0 = new
+ public int SeasonId { get; set; }
+ public bool IsNew { get; set; }
+
+ [ObservableProperty] private string _name = "";
+ [ObservableProperty] private string _description = "";
+ [ObservableProperty] private SeasonActivityType _activityType = SeasonActivityType.NpcKill;
+ [ObservableProperty] private long _targetValue;
+ [ObservableProperty] private int _bonusPoints;
+ [ObservableProperty] private int _displayOrder;
+ }
+}
+```
+
+### Task 3d: SeasonTierRow
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonTierRow.cs`:
+
+```csharp
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ public partial class SeasonTierRow : ObservableObject
+ {
+ public int Id { get; set; } // 0 = new
+ public int SeasonId { get; set; }
+ public bool IsNew { get; set; }
+
+ [ObservableProperty] private int _tierNumber;
+ [ObservableProperty] private string _tierName = "";
+ [ObservableProperty] private int _pointsRequired;
+ [ObservableProperty] private int _packageId;
+ }
+}
+```
+
+### Task 3e: SeasonLeaderboardRewardRow
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonLeaderboardRewardRow.cs`:
+
+```csharp
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ public partial class SeasonLeaderboardRewardRow : ObservableObject
+ {
+ public int Id { get; set; } // 0 = new
+ public int SeasonId { get; set; }
+ public bool IsNew { get; set; }
+
+ [ObservableProperty] private int _rankMin = 1;
+ [ObservableProperty] private int _rankMax = 1;
+ [ObservableProperty] private int _packageId;
+ }
+}
+```
+
+### Task 3f: PackageRow
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Packages\PackageRow.cs`:
+
+```csharp
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Perpetuum.AdminTool.Packages
+{
+ public partial class PackageRow : ObservableObject
+ {
+ public int Id { get; set; } // 0 = new (unsaved)
+ public bool IsNew { get; set; }
+
+ [ObservableProperty] private string _name = "";
+ [ObservableProperty] private int _itemCount;
+ [ObservableProperty] private int _seasonCount;
+
+ public bool IsUnused => SeasonCount == 0;
+ public string Display => $"{Name}";
+
+ public string SubtitleText => SeasonCount == 0
+ ? $"{ItemCount} item(s) — Not used"
+ : $"{ItemCount} item(s) — Used by {SeasonCount} season(s)";
+
+ partial void OnSeasonCountChanged(int value)
+ {
+ OnPropertyChanged(nameof(IsUnused));
+ OnPropertyChanged(nameof(SubtitleText));
+ }
+
+ partial void OnItemCountChanged(int value)
+ {
+ OnPropertyChanged(nameof(SubtitleText));
+ }
+
+ partial void OnNameChanged(string value)
+ {
+ OnPropertyChanged(nameof(Display));
+ }
+ }
+}
+```
+
+### Task 3g: PackageItemRow
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Packages\PackageItemRow.cs`:
+
+```csharp
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Perpetuum.AdminTool.Packages
+{
+ public partial class PackageItemRow : ObservableObject
+ {
+ public int Id { get; set; } // 0 = new (unsaved)
+ public int PackageId { get; set; }
+ public bool IsNew { get; set; }
+
+ [ObservableProperty] private int _definition;
+ [ObservableProperty] private int _quantity = 1;
+ [ObservableProperty] private string _displayName = "";
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 4: SeasonRepository
+
+**Goal:** Provide all SQL reads for seasons and statistics.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonRepository.cs`:
+
+```csharp
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Data.SqlClient;
+using Perpetuum.AdminTool.Settings;
+using Perpetuum.Services.Seasons;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ public class SeasonRepository
+ {
+ private readonly ConnectionSettings _connection;
+
+ public SeasonRepository(ConnectionSettings connection)
+ {
+ _connection = connection;
+ }
+
+ public async Task> LoadAllSeasonsAsync()
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT id, name, description, start_time, end_time, is_active " +
+ "FROM seasons ORDER BY start_time DESC";
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ var snap = new SeasonSnapshot
+ {
+ Id = reader.GetInt32(0),
+ Name = reader.IsDBNull(1) ? "" : reader.GetString(1),
+ Description = reader.IsDBNull(2) ? "" : reader.GetString(2),
+ StartTime = reader.GetDateTime(3),
+ EndTime = reader.GetDateTime(4),
+ IsActive = !reader.IsDBNull(5) && reader.GetBoolean(5)
+ };
+ result.Add(new SeasonRow(snap));
+ }
+ return result;
+ }
+
+ public async Task> LoadActivityRatesAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT id, season_id, activity_type, points_per_unit, unit_scale " +
+ "FROM season_activity_rates WHERE season_id = @seasonId";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new SeasonActivityRateRow
+ {
+ Id = reader.GetInt32(0),
+ SeasonId = reader.GetInt32(1),
+ ActivityType = (SeasonActivityType)reader.GetInt32(2),
+ PointsPerUnit = reader.GetDouble(3),
+ UnitScale = reader.GetInt32(4)
+ });
+ }
+ return result;
+ }
+
+ public async Task> LoadObjectivesAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT id, season_id, name, description, activity_type, " +
+ "target_value, bonus_points, display_order " +
+ "FROM season_objectives WHERE season_id = @seasonId ORDER BY display_order";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new SeasonObjectiveRow
+ {
+ Id = reader.GetInt32(0),
+ SeasonId = reader.GetInt32(1),
+ Name = reader.IsDBNull(2) ? "" : reader.GetString(2),
+ Description = reader.IsDBNull(3) ? "" : reader.GetString(3),
+ ActivityType = (SeasonActivityType)reader.GetInt32(4),
+ TargetValue = reader.GetInt64(5),
+ BonusPoints = reader.GetInt32(6),
+ DisplayOrder = reader.GetInt32(7)
+ });
+ }
+ return result;
+ }
+
+ public async Task> LoadTiersAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT id, season_id, tier_number, tier_name, points_required, package_id " +
+ "FROM season_tiers WHERE season_id = @seasonId ORDER BY tier_number";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new SeasonTierRow
+ {
+ Id = reader.GetInt32(0),
+ SeasonId = reader.GetInt32(1),
+ TierNumber = reader.GetInt32(2),
+ TierName = reader.IsDBNull(3) ? "" : reader.GetString(3),
+ PointsRequired = reader.GetInt32(4),
+ PackageId = reader.GetInt32(5)
+ });
+ }
+ return result;
+ }
+
+ public async Task> LoadLeaderboardRewardsAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT id, season_id, rank_min, rank_max, package_id " +
+ "FROM season_leaderboard_rewards WHERE season_id = @seasonId ORDER BY rank_min";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new SeasonLeaderboardRewardRow
+ {
+ Id = reader.GetInt32(0),
+ SeasonId = reader.GetInt32(1),
+ RankMin = reader.GetInt32(2),
+ RankMax = reader.GetInt32(3),
+ PackageId = reader.GetInt32(4)
+ });
+ }
+ return result;
+ }
+
+ public async Task LoadParticipantCountAsync(int seasonId)
+ {
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText = "SELECT COUNT(*) FROM season_character_points WHERE season_id = @seasonId";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ var v = await cmd.ExecuteScalarAsync();
+ return v == null ? 0 : System.Convert.ToInt32(v);
+ }
+
+ public async Task LoadActiveLast7DaysAsync(int seasonId)
+ {
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT COUNT(*) FROM season_character_points " +
+ "WHERE season_id = @seasonId AND last_updated >= DATEADD(day, -7, GETUTCDATE())";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ var v = await cmd.ExecuteScalarAsync();
+ return v == null ? 0 : System.Convert.ToInt32(v);
+ }
+
+ public async Task> LoadTierDistributionAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT t.tier_number, t.tier_name, COUNT(c.character_id) AS claim_count " +
+ "FROM season_tiers t " +
+ "LEFT JOIN season_tier_claims c ON c.tier_id = t.id AND c.season_id = @seasonId " +
+ "WHERE t.season_id = @seasonId " +
+ "GROUP BY t.id, t.tier_number, t.tier_name " +
+ "ORDER BY t.tier_number";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new TierDistributionRow(
+ reader.GetInt32(0),
+ reader.IsDBNull(1) ? "" : reader.GetString(1),
+ reader.GetInt32(2)));
+ }
+ return result;
+ }
+
+ public async Task> LoadTop10LeaderboardAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT TOP 10 scp.character_id, ch.nick AS character_name, scp.total_points " +
+ "FROM season_character_points scp " +
+ "JOIN characters ch ON ch.characterID = scp.character_id " +
+ "WHERE scp.season_id = @seasonId " +
+ "ORDER BY scp.total_points DESC";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ int rank = 1;
+ while (await reader.ReadAsync())
+ {
+ var nick = reader.IsDBNull(1) ? $"(char {reader.GetInt32(0)})" : reader.GetString(1);
+ result.Add(new LeaderboardEntryRow(rank++, nick, reader.GetInt64(2)));
+ }
+ return result;
+ }
+
+ public async Task> LoadObjectiveCompletionAsync(int seasonId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT o.id, o.name, COUNT(p.character_id) AS completed_count " +
+ "FROM season_objectives o " +
+ "LEFT JOIN season_objective_progress p ON p.objective_id = o.id " +
+ " AND p.season_id = @seasonId AND p.completed = 1 " +
+ "WHERE o.season_id = @seasonId " +
+ "GROUP BY o.id, o.name, o.display_order " +
+ "ORDER BY o.display_order";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new ObjectiveCompletionRow(
+ reader.IsDBNull(1) ? "" : reader.GetString(1),
+ reader.GetInt32(2)));
+ }
+ return result;
+ }
+
+ public async Task LoadAvgPointsPerDayAsync(int seasonId)
+ {
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT " +
+ " CAST(SUM(total_points) AS float) / " +
+ " NULLIF(COUNT(*), 0) / " +
+ " NULLIF(DATEDIFF(day, s.start_time, GETUTCDATE()), 0) AS avg_points_per_day " +
+ "FROM season_character_points scp " +
+ "JOIN seasons s ON s.id = scp.season_id " +
+ "WHERE scp.season_id = @seasonId " +
+ "GROUP BY s.start_time";
+ cmd.Parameters.AddWithValue("@seasonId", seasonId);
+ var v = await cmd.ExecuteScalarAsync();
+ if (v == null || v == System.DBNull.Value) return 0.0;
+ return System.Convert.ToDouble(v);
+ }
+ }
+
+ public record TierDistributionRow(int TierNumber, string TierName, int ClaimCount);
+ public record LeaderboardEntryRow(int Rank, string CharacterName, long TotalPoints);
+ public record ObjectiveCompletionRow(string Name, int CompletedCount);
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 5: PackageRepository
+
+**Goal:** Provide SQL reads for packages and package items, including season usage.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Packages\PackageRepository.cs`:
+
+```csharp
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Data.SqlClient;
+using Perpetuum.AdminTool.Settings;
+
+namespace Perpetuum.AdminTool.Packages
+{
+ public class PackageRepository
+ {
+ private readonly ConnectionSettings _connection;
+
+ public PackageRepository(ConnectionSettings connection)
+ {
+ _connection = connection;
+ }
+
+ public async Task> LoadAllPackagesAsync()
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT " +
+ " p.id, " +
+ " p.name, " +
+ " (SELECT COUNT(*) FROM packageitems pi WHERE pi.packageid = p.id) AS item_count, " +
+ " (SELECT COUNT(DISTINCT season_id) " +
+ " FROM ( " +
+ " SELECT season_id FROM season_tiers WHERE package_id = p.id " +
+ " UNION ALL " +
+ " SELECT season_id FROM season_leaderboard_rewards WHERE package_id = p.id " +
+ " ) refs " +
+ " ) AS season_count " +
+ "FROM packages p " +
+ "ORDER BY p.name";
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new PackageRow
+ {
+ Id = reader.GetInt32(0),
+ Name = reader.IsDBNull(1) ? "" : reader.GetString(1),
+ ItemCount = reader.GetInt32(2),
+ SeasonCount = reader.GetInt32(3)
+ });
+ }
+ return result;
+ }
+
+ public async Task> LoadPackageItemsAsync(int packageId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT id, packageid, definition, quantity " +
+ "FROM packageitems WHERE packageid = @packageId";
+ cmd.Parameters.AddWithValue("@packageId", packageId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new PackageItemRow
+ {
+ Id = reader.GetInt32(0),
+ PackageId = reader.GetInt32(1),
+ Definition = reader.GetInt32(2),
+ Quantity = reader.GetInt32(3)
+ });
+ }
+ return result;
+ }
+
+ public async Task> LoadSeasonUsageAsync(int packageId)
+ {
+ var result = new List();
+ await using var cn = new SqlConnection(_connection.BuildConnectionString());
+ await cn.OpenAsync();
+ await using var cmd = cn.CreateCommand();
+ cmd.CommandText =
+ "SELECT s.id AS season_id, s.name AS season_name, s.is_active, " +
+ " 'Tier' AS context, t.tier_name AS detail " +
+ "FROM season_tiers t " +
+ "JOIN seasons s ON s.id = t.season_id " +
+ "WHERE t.package_id = @packageId " +
+ "UNION ALL " +
+ "SELECT s.id, s.name, s.is_active, " +
+ " 'Leaderboard' AS context, " +
+ " 'Rank ' + CAST(lr.rank_min AS varchar) + '-' + CAST(lr.rank_max AS varchar) AS detail " +
+ "FROM season_leaderboard_rewards lr " +
+ "JOIN seasons s ON s.id = lr.season_id " +
+ "WHERE lr.package_id = @packageId " +
+ "ORDER BY season_name, context";
+ cmd.Parameters.AddWithValue("@packageId", packageId);
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ result.Add(new PackageUsageRow(
+ reader.GetInt32(0),
+ reader.IsDBNull(1) ? "" : reader.GetString(1),
+ !reader.IsDBNull(2) && reader.GetBoolean(2),
+ reader.IsDBNull(3) ? "" : reader.GetString(3),
+ reader.IsDBNull(4) ? "" : reader.GetString(4)));
+ }
+ return result;
+ }
+ }
+
+ public record PackageUsageRow(int SeasonId, string SeasonName, bool IsActive, string Context, string Detail);
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 6: SeasonChanges
+
+**Goal:** Build `IPendingChange` instances for all season-related mutations. Follows the `FlockChanges`/`PresenceChanges` static-class pattern.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Seasons\SeasonChanges.cs`:
+
+```csharp
+using Perpetuum.AdminTool.Editing;
+
+namespace Perpetuum.AdminTool.Seasons
+{
+ ///
+ /// Static factory of IPendingChange instances for every season mutation.
+ /// The view-models call these directly and add the result to the ChangeQueue —
+ /// there is no bulk "compute diff" helper because the seasons UI uses
+ /// per-row queue-on-save (cleaner per-detail-tab semantics than a bulk diff).
+ ///
+ public static class SeasonChanges
+ {
+ // ------------------------- seasons table -------------------------
+
+ public static IPendingChange BuildInsert(SeasonRow row)
+ {
+ return new RawSqlChange(
+ $"seasons: insert '{row.Name}' (start {row.StartTime:yyyy-MM-dd})",
+ "INSERT INTO seasons (name, description, start_time, end_time, is_active) " +
+ "VALUES (" +
+ $"{SqlLiteral.Of(row.Name)}, " +
+ $"{SqlLiteral.Of(row.Description ?? "")}, " +
+ $"{SqlLiteral.Of(row.StartTime.ToString("yyyy-MM-dd HH:mm:ss"))}, " +
+ $"{SqlLiteral.Of(row.EndTime.ToString("yyyy-MM-dd HH:mm:ss"))}, " +
+ "0)");
+ }
+
+ public static IPendingChange BuildUpdate(SeasonRow row)
+ {
+ return new RawSqlChange(
+ $"seasons: update id {row.Id} ('{row.Name}')",
+ "UPDATE seasons SET " +
+ $"name = {SqlLiteral.Of(row.Name)}, " +
+ $"description = {SqlLiteral.Of(row.Description ?? "")}, " +
+ $"start_time = {SqlLiteral.Of(row.StartTime.ToString("yyyy-MM-dd HH:mm:ss"))}, " +
+ $"end_time = {SqlLiteral.Of(row.EndTime.ToString("yyyy-MM-dd HH:mm:ss"))} " +
+ $"WHERE id = {row.Id}");
+ }
+
+ public static IPendingChange BuildActivate(int seasonId)
+ {
+ return new RawSqlChange(
+ $"seasons: activate id {seasonId}",
+ $"UPDATE seasons SET is_active = 1 WHERE id = {seasonId}");
+ }
+
+ public static IPendingChange BuildDeactivate(int seasonId)
+ {
+ return new RawSqlChange(
+ $"seasons: deactivate id {seasonId}",
+ $"UPDATE seasons SET is_active = 0 WHERE id = {seasonId}");
+ }
+
+ // ----------------------- activity rates -----------------------
+
+ public static IPendingChange BuildUpsertActivityRate(SeasonActivityRateRow row)
+ {
+ var sql =
+ "MERGE season_activity_rates AS target " +
+ $"USING (SELECT {row.SeasonId} AS season_id, {(int)row.ActivityType} AS activity_type) AS src " +
+ "ON target.season_id = src.season_id AND target.activity_type = src.activity_type " +
+ "WHEN MATCHED THEN " +
+ $" UPDATE SET points_per_unit = {SqlLiteral.Of(row.PointsPerUnit)}, unit_scale = {row.UnitScale} " +
+ "WHEN NOT MATCHED THEN " +
+ " INSERT (season_id, activity_type, points_per_unit, unit_scale) " +
+ $" VALUES ({row.SeasonId}, {(int)row.ActivityType}, {SqlLiteral.Of(row.PointsPerUnit)}, {row.UnitScale});";
+
+ return new RawSqlChange(
+ $"season_activity_rates: upsert season {row.SeasonId} type {row.ActivityType} ({row.PointsPerUnit}/unit, scale {row.UnitScale})",
+ sql);
+ }
+
+ public static IPendingChange BuildDeleteActivityRate(int id)
+ {
+ return new RawSqlChange(
+ $"season_activity_rates: delete id {id}",
+ $"DELETE FROM season_activity_rates WHERE id = {id}",
+ isDestructive: true);
+ }
+
+ // ----------------------- objectives -----------------------
+
+ public static IPendingChange BuildInsertObjective(SeasonObjectiveRow row)
+ {
+ return new RawSqlChange(
+ $"season_objectives: insert season {row.SeasonId} '{row.Name}'",
+ "INSERT INTO season_objectives " +
+ "(season_id, name, description, activity_type, target_value, bonus_points, display_order) " +
+ "VALUES (" +
+ $"{row.SeasonId}, " +
+ $"{SqlLiteral.Of(row.Name)}, " +
+ $"{SqlLiteral.Of(row.Description ?? "")}, " +
+ $"{(int)row.ActivityType}, " +
+ $"{row.TargetValue}, " +
+ $"{row.BonusPoints}, " +
+ $"{row.DisplayOrder})");
+ }
+
+ public static IPendingChange BuildUpdateObjective(SeasonObjectiveRow row)
+ {
+ return new RawSqlChange(
+ $"season_objectives: update id {row.Id} ('{row.Name}')",
+ "UPDATE season_objectives SET " +
+ $"name = {SqlLiteral.Of(row.Name)}, " +
+ $"description = {SqlLiteral.Of(row.Description ?? "")}, " +
+ $"activity_type = {(int)row.ActivityType}, " +
+ $"target_value = {row.TargetValue}, " +
+ $"bonus_points = {row.BonusPoints}, " +
+ $"display_order = {row.DisplayOrder} " +
+ $"WHERE id = {row.Id}");
+ }
+
+ public static IPendingChange BuildDeleteObjective(int id)
+ {
+ return new RawSqlChange(
+ $"season_objectives: delete id {id}",
+ $"DELETE FROM season_objectives WHERE id = {id}",
+ isDestructive: true);
+ }
+
+ // ----------------------- tiers -----------------------
+
+ public static IPendingChange BuildInsertTier(SeasonTierRow row)
+ {
+ return new RawSqlChange(
+ $"season_tiers: insert season {row.SeasonId} tier {row.TierNumber} ('{row.TierName}')",
+ "INSERT INTO season_tiers (season_id, tier_number, tier_name, points_required, package_id) " +
+ "VALUES (" +
+ $"{row.SeasonId}, " +
+ $"{row.TierNumber}, " +
+ $"{SqlLiteral.Of(row.TierName)}, " +
+ $"{row.PointsRequired}, " +
+ $"{row.PackageId})");
+ }
+
+ public static IPendingChange BuildUpdateTier(SeasonTierRow row)
+ {
+ return new RawSqlChange(
+ $"season_tiers: update id {row.Id} (tier {row.TierNumber}, '{row.TierName}')",
+ "UPDATE season_tiers SET " +
+ $"tier_number = {row.TierNumber}, " +
+ $"tier_name = {SqlLiteral.Of(row.TierName)}, " +
+ $"points_required = {row.PointsRequired}, " +
+ $"package_id = {row.PackageId} " +
+ $"WHERE id = {row.Id}");
+ }
+
+ public static IPendingChange BuildDeleteTier(int id)
+ {
+ return new RawSqlChange(
+ $"season_tiers: delete id {id}",
+ $"DELETE FROM season_tiers WHERE id = {id}",
+ isDestructive: true);
+ }
+
+ // ------------------- leaderboard rewards -------------------
+
+ public static IPendingChange BuildInsertLeaderboardReward(SeasonLeaderboardRewardRow row)
+ {
+ return new RawSqlChange(
+ $"season_leaderboard_rewards: insert season {row.SeasonId} rank {row.RankMin}-{row.RankMax}",
+ "INSERT INTO season_leaderboard_rewards (season_id, rank_min, rank_max, package_id) " +
+ "VALUES (" +
+ $"{row.SeasonId}, " +
+ $"{row.RankMin}, " +
+ $"{row.RankMax}, " +
+ $"{row.PackageId})");
+ }
+
+ public static IPendingChange BuildUpdateLeaderboardReward(SeasonLeaderboardRewardRow row)
+ {
+ return new RawSqlChange(
+ $"season_leaderboard_rewards: update id {row.Id} (rank {row.RankMin}-{row.RankMax})",
+ "UPDATE season_leaderboard_rewards SET " +
+ $"rank_min = {row.RankMin}, " +
+ $"rank_max = {row.RankMax}, " +
+ $"package_id = {row.PackageId} " +
+ $"WHERE id = {row.Id}");
+ }
+
+ public static IPendingChange BuildDeleteLeaderboardReward(int id)
+ {
+ return new RawSqlChange(
+ $"season_leaderboard_rewards: delete id {id}",
+ $"DELETE FROM season_leaderboard_rewards WHERE id = {id}",
+ isDestructive: true);
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 7: PackageChanges
+
+**Goal:** Build `IPendingChange` instances for all package and package-item mutations.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Packages\PackageChanges.cs`:
+
+```csharp
+using Perpetuum.AdminTool.Editing;
+
+namespace Perpetuum.AdminTool.Packages
+{
+ public static class PackageChanges
+ {
+ public static IPendingChange BuildInsertPackage(string name)
+ {
+ return new RawSqlChange(
+ $"packages: insert '{name}'",
+ $"INSERT INTO packages (name) VALUES ({SqlLiteral.Of(name)})");
+ }
+
+ public static IPendingChange BuildUpdatePackage(int id, string name)
+ {
+ return new RawSqlChange(
+ $"packages: update id {id} (name '{name}')",
+ $"UPDATE packages SET name = {SqlLiteral.Of(name)} WHERE id = {id}");
+ }
+
+ public static IPendingChange BuildDeletePackage(int id)
+ {
+ return new RawSqlChange(
+ $"packages: delete id {id}",
+ $"DELETE FROM packages WHERE id = {id}",
+ isDestructive: true);
+ }
+
+ public static IPendingChange BuildInsertPackageItem(int packageId, int definition, int quantity)
+ {
+ return new RawSqlChange(
+ $"packageitems: insert package {packageId} def {definition} qty {quantity}",
+ "INSERT INTO packageitems (packageid, definition, quantity) " +
+ $"VALUES ({packageId}, {definition}, {quantity})");
+ }
+
+ public static IPendingChange BuildDeletePackageItem(int id)
+ {
+ return new RawSqlChange(
+ $"packageitems: delete id {id}",
+ $"DELETE FROM packageitems WHERE id = {id}",
+ isDestructive: true);
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 8: PackagesViewModel
+
+**Goal:** Master-detail VM for the Packages view. Holds package list, currently selected package, its items, and pre-filtered picker list. Queues all mutations to `ChangeQueue`.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\ViewModels\PackagesViewModel.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Perpetuum.AdminTool.Common;
+using Perpetuum.AdminTool.Editing;
+using Perpetuum.AdminTool.Packages;
+using Perpetuum.AdminTool.Seasons;
+using Perpetuum.AdminTool.Settings;
+
+namespace Perpetuum.AdminTool.ViewModels
+{
+ public partial class PackagesViewModel : ObservableObject
+ {
+ private readonly PackageRepository _repo;
+ private readonly SeasonRepository _seasonRepo;
+ private readonly ChangeQueue _queue;
+ private readonly LookupCache _lookups;
+ private readonly ConnectionSettings _connection;
+
+ [ObservableProperty] private bool _isLoading;
+ [ObservableProperty] private string _statusMessage = "";
+ [ObservableProperty] private bool _statusIsError;
+
+ [ObservableProperty] private string _filterText = "";
+ [ObservableProperty] private PackageRow? _selectedPackage;
+
+ public ObservableCollection Packages { get; } = new();
+ public ObservableCollection FilteredPackages { get; } = new();
+ public ObservableCollection SelectedPackageItems { get; } = new();
+ public ObservableCollection SelectedPackageUsage { get; } = new();
+
+ // Pre-filtered picker list rebuilt from LookupCache on load.
+ public ObservableCollection PickItems { get; } = new();
+ private Dictionary _pickNamesByDefinition = new();
+
+ public bool HasSelection => SelectedPackage != null;
+ public bool IsActiveSeason => SelectedPackageUsage.Any(u => u.IsActive);
+ public bool CanDeleteSelected => SelectedPackage != null && SelectedPackage.SeasonCount == 0;
+
+ public string UsageDescription
+ {
+ get
+ {
+ if (SelectedPackage == null) return "";
+ if (SelectedPackageUsage.Count == 0) return "Not used by any season.";
+ var lines = SelectedPackageUsage
+ .Select(u => $"• {u.SeasonName} — {u.Context}: {u.Detail}")
+ .ToList();
+ return $"Used by {SelectedPackageUsage.Count} reference(s):\n" + string.Join("\n", lines);
+ }
+ }
+
+ public PackagesViewModel(
+ PackageRepository repo,
+ SeasonRepository seasonRepo,
+ ChangeQueue queue,
+ LookupCache lookups,
+ ConnectionSettings connection)
+ {
+ _repo = repo;
+ _seasonRepo = seasonRepo;
+ _queue = queue;
+ _lookups = lookups;
+ _connection = connection;
+ }
+
+ partial void OnFilterTextChanged(string value) => RefreshFilter();
+
+ partial void OnSelectedPackageChanged(PackageRow? value)
+ {
+ OnPropertyChanged(nameof(HasSelection));
+ OnPropertyChanged(nameof(CanDeleteSelected));
+ _ = LoadSelectedDetailAsync();
+ }
+
+ public async Task LoadAsync()
+ {
+ IsLoading = true;
+ StatusMessage = "Loading packages...";
+ StatusIsError = false;
+ try
+ {
+ var pkgs = await _repo.LoadAllPackagesAsync();
+ Packages.Clear();
+ foreach (var p in pkgs) Packages.Add(p);
+
+ RebuildPickItems();
+ RefreshFilter();
+
+ if (SelectedPackage != null)
+ {
+ var match = Packages.FirstOrDefault(p => p.Id == SelectedPackage.Id);
+ SelectedPackage = match;
+ }
+ else
+ {
+ SelectedPackageItems.Clear();
+ SelectedPackageUsage.Clear();
+ OnPropertyChanged(nameof(UsageDescription));
+ OnPropertyChanged(nameof(IsActiveSeason));
+ }
+
+ StatusMessage = $"Loaded {Packages.Count} package(s).";
+ }
+ catch (Exception ex)
+ {
+ StatusIsError = true;
+ StatusMessage = $"Load failed: {ex.Message}";
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ public void RebuildPickItems()
+ {
+ var fresh = PackageItemPickItem.BuildFilteredList(_lookups.Entities);
+ PickItems.Clear();
+ foreach (var p in fresh) PickItems.Add(p);
+ _pickNamesByDefinition = fresh.ToDictionary(p => p.Definition, p => p.DisplayName);
+ }
+
+ private void RefreshFilter()
+ {
+ FilteredPackages.Clear();
+ var f = (FilterText ?? "").Trim();
+ foreach (var p in Packages)
+ {
+ if (f.Length == 0 || p.Name.Contains(f, StringComparison.OrdinalIgnoreCase))
+ FilteredPackages.Add(p);
+ }
+ }
+
+ private async Task LoadSelectedDetailAsync()
+ {
+ SelectedPackageItems.Clear();
+ SelectedPackageUsage.Clear();
+
+ if (SelectedPackage == null || SelectedPackage.Id <= 0)
+ {
+ OnPropertyChanged(nameof(UsageDescription));
+ OnPropertyChanged(nameof(IsActiveSeason));
+ return;
+ }
+
+ try
+ {
+ var items = await _repo.LoadPackageItemsAsync(SelectedPackage.Id);
+ foreach (var it in items)
+ {
+ if (_pickNamesByDefinition.TryGetValue(it.Definition, out var name))
+ it.DisplayName = name;
+ else if (_lookups.EntityNamesByDefinition.TryGetValue(it.Definition, out var fallback))
+ it.DisplayName = fallback;
+ else
+ it.DisplayName = $"(def {it.Definition})";
+ SelectedPackageItems.Add(it);
+ }
+
+ var usage = await _repo.LoadSeasonUsageAsync(SelectedPackage.Id);
+ foreach (var u in usage) SelectedPackageUsage.Add(u);
+ }
+ catch (Exception ex)
+ {
+ StatusIsError = true;
+ StatusMessage = $"Detail load failed: {ex.Message}";
+ }
+
+ OnPropertyChanged(nameof(UsageDescription));
+ OnPropertyChanged(nameof(IsActiveSeason));
+ }
+
+ [RelayCommand]
+ private void NewPackage()
+ {
+ var name = Microsoft.VisualBasic.Interaction.InputBox(
+ "Name for the new package:", "New Package", "New Package");
+ if (string.IsNullOrWhiteSpace(name)) return;
+
+ _queue.Add(PackageChanges.BuildInsertPackage(name));
+
+ // Optimistic local add: id 0 until the next reload after commit.
+ var row = new PackageRow { Id = 0, Name = name, IsNew = true, ItemCount = 0, SeasonCount = 0 };
+ Packages.Add(row);
+ RefreshFilter();
+ SelectedPackage = row;
+ StatusIsError = false;
+ StatusMessage = $"Queued INSERT for package '{name}'. Items can be added after commit + reload.";
+ }
+
+ [RelayCommand]
+ private void DeletePackage()
+ {
+ if (SelectedPackage == null) return;
+ if (!CanDeleteSelected)
+ {
+ MessageBox.Show(
+ "Cannot delete a package that is referenced by any season. Remove its tier/leaderboard references first.",
+ "Package in use",
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+ var ok = MessageBox.Show(
+ $"Delete package '{SelectedPackage.Name}' (id {SelectedPackage.Id})? This is destructive.",
+ "Delete package",
+ MessageBoxButton.YesNo, MessageBoxImage.Warning);
+ if (ok != MessageBoxResult.Yes) return;
+
+ if (SelectedPackage.Id > 0)
+ _queue.Add(PackageChanges.BuildDeletePackage(SelectedPackage.Id));
+
+ var row = SelectedPackage;
+ SelectedPackage = null;
+ Packages.Remove(row);
+ RefreshFilter();
+ StatusIsError = false;
+ StatusMessage = $"Queued DELETE for package '{row.Name}'.";
+ }
+
+ [RelayCommand]
+ private void AddItem()
+ {
+ if (SelectedPackage == null)
+ {
+ MessageBox.Show("Select a package first.", "No package",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ if (SelectedPackage.Id <= 0)
+ {
+ MessageBox.Show(
+ "This package is unsaved. Commit the queue, then reload Packages, then add items.",
+ "Package not yet saved",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ if (PickItems.Count == 0)
+ {
+ MessageBox.Show("Picker list is empty. Reload entities and try again.",
+ "No picks available",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ // Default: pick the first allowed item with quantity 1.
+ var pick = PickItems[0];
+ _queue.Add(PackageChanges.BuildInsertPackageItem(SelectedPackage.Id, pick.Definition, 1));
+
+ var row = new PackageItemRow
+ {
+ Id = 0,
+ PackageId = SelectedPackage.Id,
+ Definition = pick.Definition,
+ Quantity = 1,
+ DisplayName = pick.DisplayName,
+ IsNew = true
+ };
+ SelectedPackageItems.Add(row);
+ SelectedPackage.ItemCount = SelectedPackage.ItemCount + 1;
+ StatusIsError = false;
+ StatusMessage = $"Queued INSERT for package item '{pick.DisplayName}' (x1). Adjust definition/quantity inline if needed.";
+ }
+
+ [RelayCommand]
+ private void RemoveItem(PackageItemRow? row)
+ {
+ if (row == null || SelectedPackage == null) return;
+ var ok = MessageBox.Show(
+ $"Remove item '{row.DisplayName}' x{row.Quantity}?",
+ "Remove item",
+ MessageBoxButton.YesNo, MessageBoxImage.Warning);
+ if (ok != MessageBoxResult.Yes) return;
+
+ if (row.Id > 0)
+ _queue.Add(PackageChanges.BuildDeletePackageItem(row.Id));
+
+ SelectedPackageItems.Remove(row);
+ SelectedPackage.ItemCount = System.Math.Max(0, SelectedPackage.ItemCount - 1);
+ StatusIsError = false;
+ StatusMessage = row.Id > 0
+ ? $"Queued DELETE for package item id {row.Id}."
+ : "Removed unsaved item.";
+ }
+ }
+}
+```
+
+> **Note on Microsoft.VisualBasic.Interaction.InputBox:** This is a built-in WPF-compatible simple input dialog. It is part of `Microsoft.VisualBasic.dll`, included with the .NET 8 Windows Desktop SDK; no extra package reference is needed.
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 9: SeasonStatisticsViewModel
+
+**Goal:** Read-only metrics view-model. Statistics load on tab activation; `RefreshCommand` reruns the queries.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\ViewModels\SeasonStatisticsViewModel.cs`:
+
+```csharp
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Perpetuum.AdminTool.Seasons;
+
+namespace Perpetuum.AdminTool.ViewModels
+{
+ public partial class SeasonStatisticsViewModel : ObservableObject
+ {
+ private readonly SeasonRepository _repo;
+
+ [ObservableProperty] private bool _isLoading;
+ [ObservableProperty] private string _statusMessage = "";
+ [ObservableProperty] private bool _statusIsError;
+ [ObservableProperty] private int _currentSeasonId;
+
+ [ObservableProperty] private int _totalParticipants;
+ [ObservableProperty] private int _activeLast7Days;
+ [ObservableProperty] private string _retentionRate = "—";
+ [ObservableProperty] private double _avgPointsPerDay;
+
+ public ObservableCollection TierDistribution { get; } = new();
+ public ObservableCollection Top10 { get; } = new();
+ public ObservableCollection ObjectiveCompletion { get; } = new();
+
+ public SeasonStatisticsViewModel(SeasonRepository repo)
+ {
+ _repo = repo;
+ }
+
+ public async Task LoadAsync(int seasonId)
+ {
+ CurrentSeasonId = seasonId;
+ if (seasonId <= 0)
+ {
+ ClearAll();
+ StatusMessage = "(unsaved season — no statistics)";
+ return;
+ }
+
+ IsLoading = true;
+ StatusMessage = "Loading statistics...";
+ StatusIsError = false;
+ try
+ {
+ TotalParticipants = await _repo.LoadParticipantCountAsync(seasonId);
+ ActiveLast7Days = await _repo.LoadActiveLast7DaysAsync(seasonId);
+ RetentionRate = TotalParticipants > 0
+ ? $"{(ActiveLast7Days * 100.0 / TotalParticipants):F1}%"
+ : "—";
+ AvgPointsPerDay = await _repo.LoadAvgPointsPerDayAsync(seasonId);
+
+ TierDistribution.Clear();
+ foreach (var t in await _repo.LoadTierDistributionAsync(seasonId))
+ TierDistribution.Add(t);
+
+ Top10.Clear();
+ foreach (var t in await _repo.LoadTop10LeaderboardAsync(seasonId))
+ Top10.Add(t);
+
+ ObjectiveCompletion.Clear();
+ foreach (var o in await _repo.LoadObjectiveCompletionAsync(seasonId))
+ ObjectiveCompletion.Add(o);
+
+ StatusMessage = $"Statistics loaded at {DateTime.Now:HH:mm:ss}.";
+ }
+ catch (Exception ex)
+ {
+ StatusIsError = true;
+ StatusMessage = $"Load failed: {ex.Message}";
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ private void ClearAll()
+ {
+ TotalParticipants = 0;
+ ActiveLast7Days = 0;
+ RetentionRate = "—";
+ AvgPointsPerDay = 0;
+ TierDistribution.Clear();
+ Top10.Clear();
+ ObjectiveCompletion.Clear();
+ }
+
+ [RelayCommand]
+ private async Task RefreshAsync()
+ {
+ if (CurrentSeasonId > 0) await LoadAsync(CurrentSeasonId);
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 10: SeasonDetailViewModel
+
+**Goal:** ViewModel for the per-season detail view (7 tabs). Loads activity rates, objectives, tiers, leaderboard. Holds nested `PackagesVm` and `StatisticsVm`.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\ViewModels\SeasonDetailViewModel.cs`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Perpetuum.AdminTool.Common;
+using Perpetuum.AdminTool.Editing;
+using Perpetuum.AdminTool.Packages;
+using Perpetuum.AdminTool.Seasons;
+using Perpetuum.AdminTool.Settings;
+using Perpetuum.Services.Seasons;
+
+namespace Perpetuum.AdminTool.ViewModels
+{
+ public partial class SeasonDetailViewModel : ObservableObject
+ {
+ private readonly SeasonRepository _repo;
+ private readonly PackageRepository _pkgRepo;
+ private readonly ChangeQueue _queue;
+ private readonly LookupCache _cache;
+ private readonly ConnectionSettings _connection;
+
+ [ObservableProperty] private SeasonRow _season;
+ [ObservableProperty] private int _selectedTabIndex;
+ [ObservableProperty] private string _statusMessage = "";
+ [ObservableProperty] private bool _statusIsError;
+
+ public ObservableCollection ActivityRates { get; } = new();
+ public ObservableCollection Objectives { get; } = new();
+ public ObservableCollection Tiers { get; } = new();
+ public ObservableCollection LeaderboardRewards { get; } = new();
+
+ // Packages reachable across the whole tab — shared with the Packages view at the
+ // SeasonsViewModel level. We just receive the same ObservableCollection here so
+ // tier/leaderboard ComboBox bindings always see the live list.
+ public ObservableCollection Packages { get; }
+
+ public PackagesViewModel PackagesVm { get; }
+ public SeasonStatisticsViewModel StatisticsVm { get; }
+
+ // Static dropdown sources for activity-type ComboBoxes inside DataGrids.
+ public IReadOnlyList ActivityTypeOptions { get; } =
+ new[]
+ {
+ new ActivityTypeOption(SeasonActivityType.NpcKill, "NPC Kill"),
+ new ActivityTypeOption(SeasonActivityType.PvpKill, "PvP Kill"),
+ new ActivityTypeOption(SeasonActivityType.MissionComplete, "Mission Complete"),
+ new ActivityTypeOption(SeasonActivityType.MineralMined, "Mineral Mined"),
+ new ActivityTypeOption(SeasonActivityType.EpSpent, "EP Spent"),
+ new ActivityTypeOption(SeasonActivityType.NicEarned, "NIC Earned"),
+ new ActivityTypeOption(SeasonActivityType.NicSpent, "NIC Spent"),
+ new ActivityTypeOption(SeasonActivityType.IntrusionPoint, "Intrusion Point"),
+ };
+
+ public bool CanActivate => !Season.IsActive;
+ public bool CanDeactivate => Season.IsActive;
+ public string StatusBadge => Season.CardState switch
+ {
+ SeasonCardState.Active => "ACTIVE",
+ SeasonCardState.Draft => "DRAFT",
+ SeasonCardState.Ended => "ENDED",
+ _ => ""
+ };
+
+ public SeasonDetailViewModel(
+ SeasonRow season,
+ SeasonRepository repo,
+ PackageRepository pkgRepo,
+ ChangeQueue queue,
+ PackagesViewModel packagesVm,
+ SeasonStatisticsViewModel statsVm,
+ LookupCache cache,
+ ConnectionSettings connection,
+ ObservableCollection packages)
+ {
+ _season = season;
+ _repo = repo;
+ _pkgRepo = pkgRepo;
+ _queue = queue;
+ _cache = cache;
+ _connection = connection;
+ PackagesVm = packagesVm;
+ StatisticsVm = statsVm;
+ Packages = packages;
+ }
+
+ public async Task LoadAsync()
+ {
+ try
+ {
+ StatusIsError = false;
+ StatusMessage = "Loading season detail...";
+
+ // Activity rates: always show all 8 types. Hydrate from DB rows where present.
+ var dbRates = Season.Id > 0
+ ? await _repo.LoadActivityRatesAsync(Season.Id)
+ : new List();
+ var dbByType = dbRates.ToDictionary(r => r.ActivityType, r => r);
+
+ ActivityRates.Clear();
+ foreach (SeasonActivityType type in Enum.GetValues(typeof(SeasonActivityType)))
+ {
+ if (dbByType.TryGetValue(type, out var existing))
+ {
+ ActivityRates.Add(existing);
+ }
+ else
+ {
+ ActivityRates.Add(new SeasonActivityRateRow
+ {
+ Id = 0,
+ SeasonId = Season.Id,
+ ActivityType = type,
+ PointsPerUnit = 0,
+ UnitScale = 1
+ });
+ }
+ }
+
+ Objectives.Clear();
+ if (Season.Id > 0)
+ foreach (var o in await _repo.LoadObjectivesAsync(Season.Id))
+ Objectives.Add(o);
+
+ Tiers.Clear();
+ if (Season.Id > 0)
+ foreach (var t in await _repo.LoadTiersAsync(Season.Id))
+ Tiers.Add(t);
+
+ LeaderboardRewards.Clear();
+ if (Season.Id > 0)
+ foreach (var l in await _repo.LoadLeaderboardRewardsAsync(Season.Id))
+ LeaderboardRewards.Add(l);
+
+ OnPropertyChanged(nameof(CanActivate));
+ OnPropertyChanged(nameof(CanDeactivate));
+ OnPropertyChanged(nameof(StatusBadge));
+ StatusMessage = $"Loaded season '{Season.Name}'.";
+ }
+ catch (Exception ex)
+ {
+ StatusIsError = true;
+ StatusMessage = $"Load failed: {ex.Message}";
+ }
+ }
+
+ partial void OnSelectedTabIndexChanged(int value)
+ {
+ // Statistics tab is index 6 (General=0, Activity=1, Objectives=2, Tiers=3,
+ // Leaderboard=4, Packages=5, Statistics=6). Load on activation.
+ if (value == 6 && Season.Id > 0)
+ _ = StatisticsVm.LoadAsync(Season.Id);
+ }
+
+ [RelayCommand]
+ private void Activate()
+ {
+ if (Season.Id <= 0)
+ {
+ MessageBox.Show("Season is unsaved. Commit the queue first.",
+ "Cannot activate", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ var ok = MessageBox.Show(
+ $"Activate season '{Season.Name}'? This queues an UPDATE seasons SET is_active = 1.",
+ "Activate season", MessageBoxButton.YesNo, MessageBoxImage.Question);
+ if (ok != MessageBoxResult.Yes) return;
+
+ _queue.Add(SeasonChanges.BuildActivate(Season.Id));
+ Season.IsActive = true;
+ OnPropertyChanged(nameof(CanActivate));
+ OnPropertyChanged(nameof(CanDeactivate));
+ OnPropertyChanged(nameof(StatusBadge));
+ StatusIsError = false;
+ StatusMessage = "Queued ACTIVATE. Use the main Commit button to apply.";
+ }
+
+ [RelayCommand]
+ private void Deactivate()
+ {
+ if (Season.Id <= 0) return;
+ var ok = MessageBox.Show(
+ $"Deactivate season '{Season.Name}'?",
+ "Deactivate season", MessageBoxButton.YesNo, MessageBoxImage.Question);
+ if (ok != MessageBoxResult.Yes) return;
+
+ _queue.Add(SeasonChanges.BuildDeactivate(Season.Id));
+ Season.IsActive = false;
+ OnPropertyChanged(nameof(CanActivate));
+ OnPropertyChanged(nameof(CanDeactivate));
+ OnPropertyChanged(nameof(StatusBadge));
+ StatusIsError = false;
+ StatusMessage = "Queued DEACTIVATE.";
+ }
+
+ [RelayCommand]
+ private void SaveGeneral()
+ {
+ if (Season.Id <= 0)
+ {
+ _queue.Add(SeasonChanges.BuildInsert(Season));
+ StatusMessage = "Queued INSERT for new season. After commit + reload, edit detail tabs.";
+ }
+ else
+ {
+ _queue.Add(SeasonChanges.BuildUpdate(Season));
+ StatusMessage = "Queued UPDATE for season general fields.";
+ }
+ StatusIsError = false;
+ }
+
+ [RelayCommand]
+ private void QueueActivityRateSave(SeasonActivityRateRow? row)
+ {
+ if (row == null) return;
+ if (Season.Id <= 0)
+ {
+ MessageBox.Show("Save the season (General tab) first.", "Season unsaved",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ row.SeasonId = Season.Id;
+ _queue.Add(SeasonChanges.BuildUpsertActivityRate(row));
+ StatusIsError = false;
+ StatusMessage = $"Queued upsert for activity '{row.ActivityType}'.";
+ }
+
+ [RelayCommand]
+ private void AddObjective()
+ {
+ if (Season.Id <= 0)
+ {
+ MessageBox.Show("Save the season (General tab) first.", "Season unsaved",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ var row = new SeasonObjectiveRow
+ {
+ SeasonId = Season.Id,
+ Name = "New Objective",
+ Description = "",
+ ActivityType = SeasonActivityType.NpcKill,
+ TargetValue = 1,
+ BonusPoints = 0,
+ DisplayOrder = Objectives.Count,
+ IsNew = true
+ };
+ Objectives.Add(row);
+ _queue.Add(SeasonChanges.BuildInsertObjective(row));
+ StatusIsError = false;
+ StatusMessage = "Queued INSERT for objective.";
+ }
+
+ [RelayCommand]
+ private void RemoveObjective(SeasonObjectiveRow? row)
+ {
+ if (row == null) return;
+ var ok = MessageBox.Show($"Remove objective '{row.Name}'?",
+ "Remove objective", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+ if (ok != MessageBoxResult.Yes) return;
+
+ if (row.Id > 0) _queue.Add(SeasonChanges.BuildDeleteObjective(row.Id));
+ Objectives.Remove(row);
+ StatusIsError = false;
+ StatusMessage = row.Id > 0
+ ? $"Queued DELETE for objective id {row.Id}."
+ : "Removed unsaved objective.";
+ }
+
+ [RelayCommand]
+ private void AddTier()
+ {
+ if (Season.Id <= 0)
+ {
+ MessageBox.Show("Save the season (General tab) first.", "Season unsaved",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ if (Packages.Count == 0)
+ {
+ MessageBox.Show("No packages exist. Create a package on the Packages tab first.",
+ "No packages", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ var row = new SeasonTierRow
+ {
+ SeasonId = Season.Id,
+ TierNumber = (Tiers.Count == 0 ? 1 : Tiers.Max(t => t.TierNumber) + 1),
+ TierName = "New Tier",
+ PointsRequired = 0,
+ PackageId = Packages[0].Id,
+ IsNew = true
+ };
+ Tiers.Add(row);
+ _queue.Add(SeasonChanges.BuildInsertTier(row));
+ StatusIsError = false;
+ StatusMessage = "Queued INSERT for tier.";
+ }
+
+ [RelayCommand]
+ private void RemoveTier(SeasonTierRow? row)
+ {
+ if (row == null) return;
+ var ok = MessageBox.Show($"Remove tier '{row.TierName}'?",
+ "Remove tier", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+ if (ok != MessageBoxResult.Yes) return;
+
+ if (row.Id > 0) _queue.Add(SeasonChanges.BuildDeleteTier(row.Id));
+ Tiers.Remove(row);
+ StatusIsError = false;
+ StatusMessage = row.Id > 0
+ ? $"Queued DELETE for tier id {row.Id}."
+ : "Removed unsaved tier.";
+ }
+
+ [RelayCommand]
+ private void AddLeaderboardReward()
+ {
+ if (Season.Id <= 0)
+ {
+ MessageBox.Show("Save the season (General tab) first.", "Season unsaved",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ if (Packages.Count == 0)
+ {
+ MessageBox.Show("No packages exist. Create a package first.",
+ "No packages", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ var nextMin = LeaderboardRewards.Count == 0 ? 1 : LeaderboardRewards.Max(r => r.RankMax) + 1;
+ var row = new SeasonLeaderboardRewardRow
+ {
+ SeasonId = Season.Id,
+ RankMin = nextMin,
+ RankMax = nextMin,
+ PackageId = Packages[0].Id,
+ IsNew = true
+ };
+ LeaderboardRewards.Add(row);
+ _queue.Add(SeasonChanges.BuildInsertLeaderboardReward(row));
+ StatusIsError = false;
+ StatusMessage = "Queued INSERT for leaderboard bracket.";
+ }
+
+ [RelayCommand]
+ private void RemoveLeaderboardReward(SeasonLeaderboardRewardRow? row)
+ {
+ if (row == null) return;
+ var ok = MessageBox.Show($"Remove rank bracket {row.RankMin}-{row.RankMax}?",
+ "Remove bracket", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+ if (ok != MessageBoxResult.Yes) return;
+
+ if (row.Id > 0) _queue.Add(SeasonChanges.BuildDeleteLeaderboardReward(row.Id));
+ LeaderboardRewards.Remove(row);
+ StatusIsError = false;
+ StatusMessage = row.Id > 0
+ ? $"Queued DELETE for leaderboard bracket id {row.Id}."
+ : "Removed unsaved bracket.";
+ }
+ }
+
+ public record ActivityTypeOption(SeasonActivityType Value, string Label);
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 11: SeasonsViewModel
+
+**Goal:** Top-level ViewModel for the tab. Holds card list, packages list (shared), detail-view drill-down state, and the switcher between Seasons / Packages views.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\ViewModels\SeasonsViewModel.cs`:
+
+```csharp
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Perpetuum.AdminTool.Common;
+using Perpetuum.AdminTool.Editing;
+using Perpetuum.AdminTool.Packages;
+using Perpetuum.AdminTool.Seasons;
+using Perpetuum.AdminTool.Settings;
+using Perpetuum.AdminTool.Views;
+
+namespace Perpetuum.AdminTool.ViewModels
+{
+ public partial class SeasonsViewModel : ObservableObject
+ {
+ private readonly SeasonRepository _seasonRepo;
+ private readonly PackageRepository _pkgRepo;
+ private readonly ChangeQueue _queue;
+ private readonly LookupCache _lookups;
+ private readonly ConnectionSettings _connection;
+
+ [ObservableProperty] private bool _isLoading;
+ [ObservableProperty] private string _statusMessage = "";
+ [ObservableProperty] private bool _statusIsError;
+
+ [ObservableProperty] private bool _showPackages;
+ [ObservableProperty] private bool _isInDetail;
+ [ObservableProperty] private SeasonDetailViewModel? _detailViewModel;
+
+ public ObservableCollection Seasons { get; } = new();
+ public PackagesViewModel PackagesVm { get; }
+
+ public bool ShowSeasonsList => !ShowPackages && !IsInDetail;
+ public bool ShowPackagesView => ShowPackages && !IsInDetail;
+
+ public SeasonsViewModel(
+ SeasonRepository seasonRepo,
+ PackageRepository pkgRepo,
+ ChangeQueue queue,
+ LookupCache lookups,
+ ConnectionSettings connection)
+ {
+ _seasonRepo = seasonRepo;
+ _pkgRepo = pkgRepo;
+ _queue = queue;
+ _lookups = lookups;
+ _connection = connection;
+ PackagesVm = new PackagesViewModel(_pkgRepo, _seasonRepo, _queue, _lookups, _connection);
+ }
+
+ partial void OnShowPackagesChanged(bool value)
+ {
+ OnPropertyChanged(nameof(ShowSeasonsList));
+ OnPropertyChanged(nameof(ShowPackagesView));
+ }
+
+ partial void OnIsInDetailChanged(bool value)
+ {
+ OnPropertyChanged(nameof(ShowSeasonsList));
+ OnPropertyChanged(nameof(ShowPackagesView));
+ }
+
+ public async Task LoadAsync()
+ {
+ IsLoading = true;
+ StatusMessage = "Loading seasons...";
+ StatusIsError = false;
+ try
+ {
+ var rows = await _seasonRepo.LoadAllSeasonsAsync();
+ Seasons.Clear();
+ foreach (var r in rows) Seasons.Add(r);
+
+ await PackagesVm.LoadAsync();
+
+ StatusMessage = $"Loaded {Seasons.Count} season(s).";
+ }
+ catch (Exception ex)
+ {
+ StatusIsError = true;
+ StatusMessage = $"Load failed: {ex.Message}";
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ [RelayCommand]
+ private void ShowSeasons()
+ {
+ IsInDetail = false;
+ ShowPackages = false;
+ }
+
+ [RelayCommand]
+ private void ShowPackagesPanel()
+ {
+ IsInDetail = false;
+ ShowPackages = true;
+ }
+
+ [RelayCommand]
+ private void BackToList()
+ {
+ IsInDetail = false;
+ DetailViewModel = null;
+ }
+
+ [RelayCommand]
+ private void NavigateToSeason(SeasonRow? row)
+ {
+ if (row == null) return;
+ var statsVm = new SeasonStatisticsViewModel(_seasonRepo);
+ var detail = new SeasonDetailViewModel(
+ row, _seasonRepo, _pkgRepo, _queue,
+ PackagesVm, statsVm,
+ _lookups, _connection, PackagesVm.Packages);
+ DetailViewModel = detail;
+ IsInDetail = true;
+ _ = detail.LoadAsync();
+ }
+
+ [RelayCommand]
+ private void NewSeason()
+ {
+ var wizardVm = new SeasonWizardViewModel(_queue, PackagesVm.Packages, () =>
+ {
+ StatusIsError = false;
+ StatusMessage = "Wizard queued INSERT statements for new season.";
+ });
+ var win = new SeasonWizardWindow(wizardVm)
+ {
+ Owner = Application.Current?.MainWindow
+ };
+ win.ShowDialog();
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed. (Note: this references `SeasonWizardViewModel` and `SeasonWizardWindow` from Tasks 12 and 16. The build will fail here if those files don't yet exist — defer running the build until Tasks 12 and 16 are also written. To unblock incremental verification, you may temporarily stub `NewSeason` to do nothing, then restore it once the wizard exists.)
+
+> **Recommended order to keep builds green:** Skip the `NewSeason` command body initially (replace its body with `{ /* wired in Task 16 */ }`), and after Tasks 12 and 16 are done, restore the body shown above.
+
+---
+
+## Task 12: SeasonWizardViewModel
+
+**Goal:** 6-step wizard ViewModel. Collects all season config, then queues a SeasonChanges.BuildInsert + objective/tier/leaderboard/rate inserts. Note: child inserts assume the season identity hasn't been resolved yet — the spec accepts this limitation by queuing all inserts; users commit the queue then reload and revisit details.
+
+Actually, the wizard cannot queue child INSERTs that reference the new season's id (it doesn't exist until commit). To handle this cleanly, the wizard queues only the season INSERT; the user is instructed to commit, reload, and finish configuring via the detail tabs. The wizard's review step lists what it will queue and what must be done after commit.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\ViewModels\SeasonWizardViewModel.cs`:
+
+```csharp
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Perpetuum.AdminTool.Editing;
+using Perpetuum.AdminTool.Packages;
+using Perpetuum.AdminTool.Seasons;
+using Perpetuum.Services.Seasons;
+
+namespace Perpetuum.AdminTool.ViewModels
+{
+ public partial class SeasonWizardViewModel : ObservableObject
+ {
+ private readonly ChangeQueue _queue;
+ private readonly ObservableCollection _packages;
+ private readonly Action _onComplete;
+
+ // Step 1 of 6: Season Info
+ // Step 2 of 6: Activity Rates
+ // Step 3 of 6: Objectives
+ // Step 4 of 6: Tiers
+ // Step 5 of 6: Leaderboard Rewards
+ // Step 6 of 6: Review
+ [ObservableProperty] private int _currentStep = 1;
+
+ public bool IsStep1 => CurrentStep == 1;
+ public bool IsStep2 => CurrentStep == 2;
+ public bool IsStep3 => CurrentStep == 3;
+ public bool IsStep4 => CurrentStep == 4;
+ public bool IsStep5 => CurrentStep == 5;
+ public bool IsReviewStep => CurrentStep == 6;
+
+ public bool CanGoBack => CurrentStep > 1;
+ public bool CanGoNext => CurrentStep < 6;
+
+ public string StepTitle => CurrentStep switch
+ {
+ 1 => "Step 1 of 6 — Season Info",
+ 2 => "Step 2 of 6 — Activity Rates",
+ 3 => "Step 3 of 6 — Objectives (optional)",
+ 4 => "Step 4 of 6 — Tiers (optional)",
+ 5 => "Step 5 of 6 — Leaderboard Rewards (optional)",
+ 6 => "Step 6 of 6 — Review",
+ _ => ""
+ };
+
+ // Step 1 fields
+ [ObservableProperty] private string _name = "";
+ [ObservableProperty] private string _description = "";
+ [ObservableProperty] private DateTime _startTime = DateTime.UtcNow.Date;
+ [ObservableProperty] private DateTime _endTime = DateTime.UtcNow.Date.AddDays(30);
+
+ // Step 2 — 8 pre-populated rows
+ public ObservableCollection ActivityRates { get; } = new();
+ // Step 3
+ public ObservableCollection Objectives { get; } = new();
+ // Step 4
+ public ObservableCollection Tiers { get; } = new();
+ // Step 5
+ public ObservableCollection LeaderboardRewards { get; } = new();
+
+ public ObservableCollection Packages => _packages;
+ public bool HasPackages => _packages.Count > 0;
+
+ public string Step1Validation { get; private set; } = "";
+ public string FinishHint => Tiers.Count > 0 || LeaderboardRewards.Count > 0 || Objectives.Count > 0
+ ? "After committing the season INSERT, reopen the season detail to add objectives, tiers, and leaderboard rewards (they need the assigned season id)."
+ : "Click 'Add to Change Queue' to queue the INSERT for the new season.";
+
+ public SeasonWizardViewModel(
+ ChangeQueue queue,
+ ObservableCollection packages,
+ Action onComplete)
+ {
+ _queue = queue;
+ _packages = packages;
+ _onComplete = onComplete;
+
+ // Pre-populate all 8 activity types with disabled defaults.
+ foreach (SeasonActivityType type in Enum.GetValues(typeof(SeasonActivityType)))
+ {
+ ActivityRates.Add(new SeasonActivityRateRow
+ {
+ Id = 0,
+ SeasonId = 0,
+ ActivityType = type,
+ PointsPerUnit = 0,
+ UnitScale = 1
+ });
+ }
+ }
+
+ partial void OnCurrentStepChanged(int value)
+ {
+ OnPropertyChanged(nameof(IsStep1));
+ OnPropertyChanged(nameof(IsStep2));
+ OnPropertyChanged(nameof(IsStep3));
+ OnPropertyChanged(nameof(IsStep4));
+ OnPropertyChanged(nameof(IsStep5));
+ OnPropertyChanged(nameof(IsReviewStep));
+ OnPropertyChanged(nameof(CanGoBack));
+ OnPropertyChanged(nameof(CanGoNext));
+ OnPropertyChanged(nameof(StepTitle));
+ OnPropertyChanged(nameof(FinishHint));
+ }
+
+ private bool ValidateCurrentStep()
+ {
+ Step1Validation = "";
+ OnPropertyChanged(nameof(Step1Validation));
+
+ if (CurrentStep == 1)
+ {
+ if (string.IsNullOrWhiteSpace(Name))
+ {
+ Step1Validation = "Name is required.";
+ OnPropertyChanged(nameof(Step1Validation));
+ return false;
+ }
+ if (EndTime <= StartTime)
+ {
+ Step1Validation = "End time must be after start time.";
+ OnPropertyChanged(nameof(Step1Validation));
+ return false;
+ }
+ }
+ return true;
+ }
+
+ [RelayCommand]
+ private void Back()
+ {
+ if (CurrentStep > 1) CurrentStep--;
+ }
+
+ [RelayCommand]
+ private void Next()
+ {
+ if (!ValidateCurrentStep()) return;
+ if (CurrentStep < 6) CurrentStep++;
+ }
+
+ [RelayCommand]
+ private void AddObjectiveRow()
+ {
+ Objectives.Add(new SeasonObjectiveRow
+ {
+ SeasonId = 0,
+ Name = "New Objective",
+ ActivityType = SeasonActivityType.NpcKill,
+ TargetValue = 1,
+ BonusPoints = 0,
+ DisplayOrder = Objectives.Count,
+ IsNew = true
+ });
+ }
+
+ [RelayCommand]
+ private void RemoveObjectiveRow(SeasonObjectiveRow? row)
+ {
+ if (row != null) Objectives.Remove(row);
+ }
+
+ [RelayCommand]
+ private void AddTierRow()
+ {
+ if (Packages.Count == 0) return;
+ Tiers.Add(new SeasonTierRow
+ {
+ SeasonId = 0,
+ TierNumber = Tiers.Count == 0 ? 1 : Tiers.Max(t => t.TierNumber) + 1,
+ TierName = "New Tier",
+ PointsRequired = 0,
+ PackageId = Packages[0].Id,
+ IsNew = true
+ });
+ }
+
+ [RelayCommand]
+ private void RemoveTierRow(SeasonTierRow? row)
+ {
+ if (row != null) Tiers.Remove(row);
+ }
+
+ [RelayCommand]
+ private void AddLeaderboardRow()
+ {
+ if (Packages.Count == 0) return;
+ var nextMin = LeaderboardRewards.Count == 0 ? 1 : LeaderboardRewards.Max(r => r.RankMax) + 1;
+ LeaderboardRewards.Add(new SeasonLeaderboardRewardRow
+ {
+ SeasonId = 0,
+ RankMin = nextMin,
+ RankMax = nextMin,
+ PackageId = Packages[0].Id,
+ IsNew = true
+ });
+ }
+
+ [RelayCommand]
+ private void RemoveLeaderboardRow(SeasonLeaderboardRewardRow? row)
+ {
+ if (row != null) LeaderboardRewards.Remove(row);
+ }
+
+ [RelayCommand]
+ private void Finish()
+ {
+ // Queue ONLY the season INSERT. Children require the new season id which
+ // is not known until commit; we surface this clearly via FinishHint and ask
+ // the admin to commit + reload + finish configuration via the detail tabs.
+ var seed = new SeasonSnapshot
+ {
+ Id = 0,
+ Name = Name,
+ Description = Description ?? "",
+ StartTime = StartTime,
+ EndTime = EndTime,
+ IsActive = false
+ };
+ var row = SeasonRow.CreateNew(seed);
+ _queue.Add(SeasonChanges.BuildInsert(row));
+
+ _onComplete?.Invoke();
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 13: PackagesView (XAML)
+
+**Goal:** Master-detail UI for packages.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\PackagesView.xaml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\PackagesView.xaml.cs`:
+
+```csharp
+using System.Windows;
+using System.Windows.Controls;
+using Perpetuum.AdminTool.ViewModels;
+
+namespace Perpetuum.AdminTool.Views
+{
+ public partial class PackagesView : UserControl
+ {
+ public PackagesView()
+ {
+ InitializeComponent();
+ }
+
+ private PackagesViewModel? Vm => DataContext as PackagesViewModel;
+
+ private async void OnReloadClick(object sender, RoutedEventArgs e)
+ {
+ if (Vm == null) return;
+ await Vm.LoadAsync();
+ }
+ }
+}
+```
+
+> **App-level resources to add:** The `BoolToVisibilityHidden` static resource referenced in the XAML must exist application-wide. If it doesn't yet, define it in `App.xaml`:
+>
+> ```xml
+>
+>
+>
+> ```
+>
+> Check `App.xaml` first; only add if missing.
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 14: SeasonDetailView (XAML)
+
+**Goal:** Tabbed detail UI for one season.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\SeasonDetailView.xaml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\SeasonDetailView.xaml.cs`:
+
+```csharp
+using System.Windows;
+using System.Windows.Controls;
+using Perpetuum.AdminTool.ViewModels;
+
+namespace Perpetuum.AdminTool.Views
+{
+ public partial class SeasonDetailView : UserControl
+ {
+ public SeasonDetailView()
+ {
+ InitializeComponent();
+ }
+
+ private SeasonDetailViewModel? Vm => DataContext as SeasonDetailViewModel;
+
+ private void OnBackClick(object sender, RoutedEventArgs e)
+ {
+ // The parent SeasonsView listens via the bound ViewModel — but for the simpler
+ // detail-view case the back-arrow lives in the host. We surface this via a
+ // routed event consumed by the parent SeasonsView's BackCommand wiring.
+ if (DataContext is SeasonDetailViewModel)
+ {
+ // Bubble a request: parent SeasonsView wires its own button outside this
+ // control. Here we walk up to the SeasonsView and invoke its back command.
+ var parent = FindAncestor(this);
+ parent?.RequestBack();
+ }
+ }
+
+ private static T? FindAncestor(DependencyObject child) where T : DependencyObject
+ {
+ var current = System.Windows.Media.VisualTreeHelper.GetParent(child);
+ while (current != null && current is not T)
+ current = System.Windows.Media.VisualTreeHelper.GetParent(current);
+ return current as T;
+ }
+ }
+}
+```
+
+> **Note:** `SeasonsView.RequestBack()` is defined in Task 15.
+
+- [ ] Run verification command. Build must succeed (Task 15 must be present for the `SeasonsView` reference to resolve; if running this task standalone, comment out the `RequestBack()` call temporarily).
+
+---
+
+## Task 15: SeasonsView (XAML)
+
+**Goal:** Top-level tab view. Holds the Seasons/Packages segmented switcher, the season cards grid (when in seasons mode), and acts as a host that swaps in `PackagesView` or `SeasonDetailView` as needed.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\SeasonsView.xaml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\SeasonsView.xaml.cs`:
+
+```csharp
+using System.Windows;
+using System.Windows.Controls;
+using Perpetuum.AdminTool.ViewModels;
+
+namespace Perpetuum.AdminTool.Views
+{
+ public partial class SeasonsView : UserControl
+ {
+ public SeasonsView()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ }
+
+ private SeasonsViewModel? Vm => DataContext as SeasonsViewModel;
+
+ private async void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ if (Vm != null && Vm.Seasons.Count == 0)
+ await Vm.LoadAsync();
+ }
+
+ private async void OnReloadClick(object sender, RoutedEventArgs e)
+ {
+ if (Vm == null) return;
+ await Vm.LoadAsync();
+ }
+
+ // Invoked by SeasonDetailView's back arrow via VisualTree walk-up.
+ public void RequestBack()
+ {
+ Vm?.BackToListCommand.Execute(null);
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 16: SeasonWizardWindow (XAML)
+
+**Goal:** Modal 6-step wizard window.
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\SeasonWizardWindow.xaml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+- [ ] Write file `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\SeasonWizardWindow.xaml.cs`:
+
+```csharp
+using System.Windows;
+using Perpetuum.AdminTool.ViewModels;
+
+namespace Perpetuum.AdminTool.Views
+{
+ public partial class SeasonWizardWindow : Window
+ {
+ private readonly SeasonWizardViewModel _vm;
+
+ public SeasonWizardWindow(SeasonWizardViewModel vm)
+ {
+ InitializeComponent();
+ _vm = vm;
+ DataContext = vm;
+ }
+
+ private void OnFinishOrNextClick(object sender, RoutedEventArgs e)
+ {
+ if (_vm.IsReviewStep)
+ {
+ _vm.FinishCommand.Execute(null);
+ DialogResult = true;
+ Close();
+ }
+ else
+ {
+ _vm.NextCommand.Execute(null);
+ }
+ }
+ }
+}
+```
+
+- [ ] Run verification command. Build must succeed.
+
+---
+
+## Task 17: Wire SeasonsViewModel into MainViewModel and MainWindow
+
+**Goal:** Construct and expose the `SeasonsViewModel` from `MainViewModel`, and add a `` for it in `MainWindow.xaml`. Also ensure `App.xaml` exposes the `BoolToVisibilityHidden` resource referenced in the views.
+
+- [ ] Inspect `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\App.xaml`. If a `` is not registered with key `BoolToVisibilityHidden`, add it under ``. The full file should look like (preserve any existing resources):
+
+```xml
+
+
+
+
+
+```
+
+> If the file already has ``, only insert the ` ` element inside it.
+
+- [ ] Edit `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\ViewModels\MainViewModel.cs`. Add three changes:
+
+ 1. Add `using` directives at the top of the file (alongside existing usings):
+
+```csharp
+using Perpetuum.AdminTool.Packages;
+using Perpetuum.AdminTool.Seasons;
+```
+
+ 2. Add a new public property next to the existing `Flocks` property:
+
+```csharp
+public SeasonsViewModel Seasons { get; }
+```
+
+ 3. Inside the constructor, after the line `Flocks = new FlocksViewModel(store, session.Changes, session.Lookups);`, add:
+
+```csharp
+ Seasons = new SeasonsViewModel(
+ new SeasonRepository(store.Settings.Connection),
+ new PackageRepository(store.Settings.Connection),
+ session.Changes,
+ session.Lookups,
+ store.Settings.Connection);
+```
+
+- [ ] Edit `E:\MyStuff\Projects\PerpetuumServer2\src\Perpetuum.AdminTool\Views\MainWindow.xaml`. Add a `` inside the existing ``, placed right after the `` block. Insert:
+
+```xml
+
+
+
+```
+
+- [ ] Run final verification: `dotnet build E:\MyStuff\Projects\PerpetuumServer2\PerpetuumServer2.sln -c Release -p:Platform=x64`. Build must succeed with no errors. Warnings may remain (existing baseline) but must not be regressed.
+
+---
+
+## Self-review Checklist
+
+Before declaring the plan complete, verify:
+
+- [x] Every spec section from `2026-05-10-seasons-admin-tool-design.md` has a corresponding task:
+ - LookupCache `hidden` column change → Task 1
+ - Entity Picker filtering by 11 root flags → Task 2
+ - Row models for `seasons`, `season_activity_rates`, `season_objectives`, `season_tiers`, `season_leaderboard_rewards`, `packages`, `packageitems` → Task 3
+ - Repository SQL for all reads (seasons + statistics) → Task 4
+ - Repository SQL for packages and usage → Task 5
+ - Change objects for every season mutation (insert/update/activate/deactivate, upsert activity rate, CRUD for objectives/tiers/leaderboard) → Task 6
+ - Change objects for packages and package items → Task 7
+ - Master-detail packages VM → Task 8
+ - Statistics VM (read-only, lazy-load) → Task 9
+ - Per-season detail VM (7 tabs including Statistics activation hook) → Task 10
+ - Top-level tab VM (segmented switcher + drill-down state) → Task 11
+ - 6-step wizard VM → Task 12
+ - PackagesView XAML + code-behind → Task 13
+ - SeasonDetailView XAML + code-behind (all 7 tabs) → Task 14
+ - SeasonsView XAML + code-behind (cards, switcher, host) → Task 15
+ - SeasonWizardWindow XAML + code-behind → Task 16
+ - Bootstrapper wiring in MainViewModel + MainWindow + App resources → Task 17
+- [x] No `TBD`, `TODO`, or "similar to" placeholders in any task.
+- [x] All ViewModel constructor signatures match what the consuming code expects:
+ - `PackagesViewModel(PackageRepository, SeasonRepository, ChangeQueue, LookupCache, ConnectionSettings)` — consumed by SeasonsViewModel.
+ - `SeasonDetailViewModel(SeasonRow, SeasonRepository, PackageRepository, ChangeQueue, PackagesViewModel, SeasonStatisticsViewModel, LookupCache, ConnectionSettings, ObservableCollection)` — consumed by SeasonsViewModel.NavigateToSeason.
+ - `SeasonWizardViewModel(ChangeQueue, ObservableCollection, Action)` — consumed by SeasonsViewModel.NewSeason.
+ - `SeasonStatisticsViewModel(SeasonRepository)` — consumed by SeasonsViewModel.NavigateToSeason.
+- [x] All file paths are absolute and rooted in `E:\MyStuff\Projects\PerpetuumServer2\`.
+- [x] The verification command is identical for every task: `dotnet build E:\MyStuff\Projects\PerpetuumServer2\PerpetuumServer2.sln -c Release -p:Platform=x64`.
+- [x] All XAML files declare `xmlns` for any used clr-namespace (common, vm, views, d, mc).
+- [x] Activity-type ComboBoxes (in detail and wizard DataGrids) bind to `ActivityTypeOptions` (a static read-only list of `{Value, Label}` pairs in `SeasonDetailViewModel`).
+- [x] Package ComboBoxes (tiers and leaderboard) bind to the shared `Packages` collection.
+- [x] The `RemoveItemCommand` (PackagesViewModel) takes a `PackageItemRow?` parameter; XAML passes the row as `CommandParameter`.
+
+---
+
diff --git a/docs/superpowers/plans/2026-05-10-seasons-system.md b/docs/superpowers/plans/2026-05-10-seasons-system.md
new file mode 100644
index 0000000..fce22b1
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-10-seasons-system.md
@@ -0,0 +1,1908 @@
+# Seasons System Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a database-driven Seasons system that awards players points for eight activity types, delivers tier rewards via the existing `accountredeemableitems` redeem flow, ranks players on a leaderboard at season end, and delivers all feedback through in-game mail.
+
+**Architecture:** A singleton `SeasonService` (extending `Process`) caches the active season every 5 minutes and runs a 1-minute end-of-season check. Activity hooks in existing game classes call `SeasonServiceLocator.Instance?.RecordActivity(...)` — a static locator avoids constructor changes across 7+ classes that are not Autofac-managed. All reward delivery goes through `packageitems` → `accountredeemableitems`. Admin configuration uses the existing `[ChatCommand]` secured-channel pattern.
+
+**Tech Stack:** .NET 8 / C#, SQL Server, Autofac DI, `Db.Query()` fluent SQL builder, `MailHandler`, `IProcessManager` timed loop.
+
+---
+
+## File Structure
+
+### New Files
+
+| File | Responsibility |
+|---|---|
+| `docs/Patches/p36.0/Features/Seasons/migration.sql` | Creates all 8 season tables |
+| `src/Perpetuum/Services/Seasons/SeasonActivityType.cs` | `SeasonActivityType` enum |
+| `src/Perpetuum/Services/Seasons/SeasonModels.cs` | Domain models |
+| `src/Perpetuum/Services/Seasons/ISeasonService.cs` | Service contract |
+| `src/Perpetuum/Services/Seasons/SeasonRepository.cs` | All DB access for season tables |
+| `src/Perpetuum/Services/Seasons/SeasonService.cs` | Core service: cache, `RecordActivity`, end-of-season, mail |
+| `src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs` | Static locator for non-Autofac call sites |
+| `src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs` | Autofac registration |
+| `src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs` | Admin chat commands |
+
+### Modified Files
+
+| File | Change |
+|---|---|
+| `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs` | Load `SeasonModule`; discover `SeasonAdminCommandHandlers` |
+| `src/Perpetuum/Zones/NpcSystem/Npc.cs` | NPC kill hook |
+| `src/Perpetuum/Players/Player.cs` | PvP kill hook + login hook |
+| `src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs` | Mission complete hook |
+| `src/Perpetuum/Accounting/Characters/CharacterWallet.cs` | NIC earned/spent hook |
+| `src/Perpetuum/Accounting/AccountManager.cs` | EP spent hook |
+| `src/Perpetuum/Zones/Intrusion/Outpost.cs` | Intrusion/SAP hook |
+| `src/Perpetuum/Modules/DrillerModule.cs` | Mineral mined hook |
+| `src/Perpetuum/Modules/LargeDrillerModule.cs` | Mineral mined hook (second driller variant) |
+
+---
+
+## Task 1: SQL Migration Script
+
+**Files:**
+- Create: `docs/Patches/p36.0/Features/Seasons/migration.sql`
+
+- [ ] **Step 1: Create the migration directory and file**
+
+```
+mkdir docs\Patches\p36.0\Features\Seasons
+```
+
+Create `docs/Patches/p36.0/Features/Seasons/migration.sql` with this content:
+
+```sql
+-- Seasons System Migration
+-- Run once against the game database before deploying the updated server binary.
+
+CREATE TABLE seasons (
+ id INT IDENTITY(1,1) NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ description VARCHAR(512) NOT NULL DEFAULT '',
+ start_time DATETIME NOT NULL,
+ end_time DATETIME NOT NULL,
+ is_active BIT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_seasons PRIMARY KEY (id)
+);
+
+CREATE TABLE season_activity_rates (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ activity_type INT NOT NULL,
+ points_per_unit FLOAT NOT NULL,
+ unit_scale INT NOT NULL DEFAULT 1,
+ CONSTRAINT PK_season_activity_rates PRIMARY KEY (id)
+);
+
+CREATE TABLE season_objectives (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ name VARCHAR(128) NOT NULL,
+ description VARCHAR(512) NOT NULL DEFAULT '',
+ activity_type INT NOT NULL,
+ target_value BIGINT NOT NULL,
+ bonus_points INT NOT NULL,
+ display_order INT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_season_objectives PRIMARY KEY (id)
+);
+
+CREATE TABLE season_tiers (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ tier_number INT NOT NULL,
+ tier_name VARCHAR(64) NOT NULL,
+ points_required INT NOT NULL,
+ package_id INT NOT NULL,
+ CONSTRAINT PK_season_tiers PRIMARY KEY (id)
+);
+
+CREATE TABLE season_leaderboard_rewards (
+ id INT IDENTITY(1,1) NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ rank_min INT NOT NULL,
+ rank_max INT NOT NULL,
+ package_id INT NOT NULL,
+ CONSTRAINT PK_season_leaderboard_rewards PRIMARY KEY (id)
+);
+
+CREATE TABLE season_character_points (
+ character_id INT NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ total_points BIGINT NOT NULL DEFAULT 0,
+ last_updated DATETIME NOT NULL DEFAULT GETUTCDATE(),
+ intro_mail_sent BIT NOT NULL DEFAULT 0,
+ leaderboard_reward_delivered BIT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_season_character_points PRIMARY KEY (character_id, season_id)
+);
+
+CREATE TABLE season_objective_progress (
+ character_id INT NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ objective_id INT NOT NULL REFERENCES season_objectives(id),
+ current_value BIGINT NOT NULL DEFAULT 0,
+ completed BIT NOT NULL DEFAULT 0,
+ completed_time DATETIME NULL,
+ bonus_awarded BIT NOT NULL DEFAULT 0,
+ CONSTRAINT PK_season_objective_progress PRIMARY KEY (character_id, season_id, objective_id)
+);
+
+CREATE TABLE season_tier_claims (
+ character_id INT NOT NULL,
+ season_id INT NOT NULL REFERENCES seasons(id),
+ tier_id INT NOT NULL REFERENCES season_tiers(id),
+ claimed_time DATETIME NOT NULL DEFAULT GETUTCDATE(),
+ CONSTRAINT PK_season_tier_claims PRIMARY KEY (character_id, season_id, tier_id)
+);
+
+-- Indexes for common query patterns
+CREATE INDEX IX_season_character_points_season ON season_character_points (season_id, total_points DESC);
+CREATE INDEX IX_season_objective_progress_char ON season_objective_progress (character_id, season_id);
+CREATE INDEX IX_season_tier_claims_char ON season_tier_claims (character_id, season_id);
+```
+
+- [ ] **Step 2: Run migration against the database**
+
+Apply the script to the game database using your SQL Server management tool of choice. Verify all 8 tables exist with no errors.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/Patches/p36.0/Features/Seasons/migration.sql
+git commit -m "feat(seasons): add SQL migration for 8 season tables"
+```
+
+---
+
+## Task 2: Enum, Domain Models, and Service Interface
+
+**Files:**
+- Create: `src/Perpetuum/Services/Seasons/SeasonActivityType.cs`
+- Create: `src/Perpetuum/Services/Seasons/SeasonModels.cs`
+- Create: `src/Perpetuum/Services/Seasons/ISeasonService.cs`
+
+- [ ] **Step 1: Create `SeasonActivityType.cs`**
+
+```csharp
+namespace Perpetuum.Services.Seasons
+{
+ public enum SeasonActivityType
+ {
+ NpcKill = 1,
+ PvpKill = 2,
+ MissionComplete = 3,
+ MineralMined = 4,
+ EpSpent = 5,
+ NicEarned = 6,
+ NicSpent = 7,
+ IntrusionPoint = 8,
+ }
+}
+```
+
+- [ ] **Step 2: Create `SeasonModels.cs`**
+
+```csharp
+using System;
+using System.Collections.Generic;
+
+namespace Perpetuum.Services.Seasons
+{
+ public class Season
+ {
+ public int Id { get; set; }
+ public string Name { get; set; } = "";
+ public string Description { get; set; } = "";
+ public DateTime StartTime { get; set; }
+ public DateTime EndTime { get; set; }
+ public bool IsActive { get; set; }
+ }
+
+ public class SeasonActivityRate
+ {
+ public int Id { get; set; }
+ public int SeasonId { get; set; }
+ public SeasonActivityType ActivityType { get; set; }
+ public double PointsPerUnit { get; set; }
+ public int UnitScale { get; set; }
+ }
+
+ public class SeasonObjective
+ {
+ public int Id { get; set; }
+ public int SeasonId { get; set; }
+ public string Name { get; set; } = "";
+ public string Description { get; set; } = "";
+ public SeasonActivityType ActivityType { get; set; }
+ public long TargetValue { get; set; }
+ public int BonusPoints { get; set; }
+ public int DisplayOrder { get; set; }
+ }
+
+ public class SeasonTier
+ {
+ public int Id { get; set; }
+ public int SeasonId { get; set; }
+ public int TierNumber { get; set; }
+ public string TierName { get; set; } = "";
+ public int PointsRequired { get; set; }
+ public int PackageId { get; set; }
+ }
+
+ public class SeasonLeaderboardReward
+ {
+ public int Id { get; set; }
+ public int SeasonId { get; set; }
+ public int RankMin { get; set; }
+ public int RankMax { get; set; }
+ public int PackageId { get; set; }
+ }
+
+ public class SeasonCharacterPoints
+ {
+ public int CharacterId { get; set; }
+ public int SeasonId { get; set; }
+ public long TotalPoints { get; set; }
+ public bool IntroMailSent { get; set; }
+ public bool LeaderboardRewardDelivered { get; set; }
+ }
+
+ public class SeasonPackageItem
+ {
+ public int Definition { get; set; }
+ public int Quantity { get; set; }
+ }
+}
+```
+
+- [ ] **Step 3: Create `ISeasonService.cs`**
+
+```csharp
+using Perpetuum.Accounting.Characters;
+
+namespace Perpetuum.Services.Seasons
+{
+ public interface ISeasonService
+ {
+ void RecordActivity(int characterId, SeasonActivityType type, long amount);
+ void OnCharacterLogin(Character character);
+ }
+}
+```
+
+- [ ] **Step 4: Build to verify no compile errors**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: build succeeds (new files compile cleanly).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonActivityType.cs
+git add src/Perpetuum/Services/Seasons/SeasonModels.cs
+git add src/Perpetuum/Services/Seasons/ISeasonService.cs
+git commit -m "feat(seasons): add SeasonActivityType enum, domain models, ISeasonService"
+```
+
+---
+
+## Task 3: SeasonRepository
+
+**Files:**
+- Create: `src/Perpetuum/Services/Seasons/SeasonRepository.cs`
+
+The repository follows the `AccountRepository` pattern: `Db.Query().CommandText(sql).SetParameter(...).Execute()`. All upserts use `MERGE ... WITH (HOLDLOCK)` to be safe under concurrent writes.
+
+- [ ] **Step 1: Create `SeasonRepository.cs`**
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Perpetuum.Data;
+
+namespace Perpetuum.Services.Seasons
+{
+ public class SeasonRepository
+ {
+ // ── Cache loading ────────────────────────────────────────────────────
+
+ public Season? GetActiveSeason()
+ {
+ var record = Db.Query()
+ .CommandText("SELECT id, name, description, start_time, end_time, is_active " +
+ "FROM seasons WHERE is_active = 1")
+ .ExecuteSingleRow();
+
+ if (record == null) return null;
+
+ return new Season
+ {
+ Id = record.GetValue("id"),
+ Name = record.GetValue("name"),
+ Description = record.GetValue("description"),
+ StartTime = record.GetValue("start_time"),
+ EndTime = record.GetValue("end_time"),
+ IsActive = record.GetValue("is_active"),
+ };
+ }
+
+ public List GetActivityRates(int seasonId)
+ {
+ return Db.Query()
+ .CommandText("SELECT id, season_id, activity_type, points_per_unit, unit_scale " +
+ "FROM season_activity_rates WHERE season_id = @seasonId")
+ .SetParameter("@seasonId", seasonId)
+ .Execute()
+ .Select(r => new SeasonActivityRate
+ {
+ Id = r.GetValue("id"),
+ SeasonId = r.GetValue("season_id"),
+ ActivityType = (SeasonActivityType)r.GetValue("activity_type"),
+ PointsPerUnit = r.GetValue("points_per_unit"),
+ UnitScale = r.GetValue("unit_scale"),
+ })
+ .ToList();
+ }
+
+ public List GetObjectives(int seasonId)
+ {
+ return Db.Query()
+ .CommandText("SELECT id, season_id, name, description, activity_type, " +
+ "target_value, bonus_points, display_order " +
+ "FROM season_objectives WHERE season_id = @seasonId")
+ .SetParameter("@seasonId", seasonId)
+ .Execute()
+ .Select(r => new SeasonObjective
+ {
+ Id = r.GetValue("id"),
+ SeasonId = r.GetValue("season_id"),
+ Name = r.GetValue("name"),
+ Description = r.GetValue("description"),
+ ActivityType = (SeasonActivityType)r.GetValue("activity_type"),
+ TargetValue = r.GetValue("target_value"),
+ BonusPoints = r.GetValue("bonus_points"),
+ DisplayOrder = r.GetValue("display_order"),
+ })
+ .ToList();
+ }
+
+ public List GetTiers(int seasonId)
+ {
+ return Db.Query()
+ .CommandText("SELECT id, season_id, tier_number, tier_name, points_required, package_id " +
+ "FROM season_tiers WHERE season_id = @seasonId ORDER BY tier_number")
+ .SetParameter("@seasonId", seasonId)
+ .Execute()
+ .Select(r => new SeasonTier
+ {
+ Id = r.GetValue("id"),
+ SeasonId = r.GetValue("season_id"),
+ TierNumber = r.GetValue("tier_number"),
+ TierName = r.GetValue("tier_name"),
+ PointsRequired = r.GetValue("points_required"),
+ PackageId = r.GetValue("package_id"),
+ })
+ .ToList();
+ }
+
+ public List GetLeaderboardRewards(int seasonId)
+ {
+ return Db.Query()
+ .CommandText("SELECT id, season_id, rank_min, rank_max, package_id " +
+ "FROM season_leaderboard_rewards WHERE season_id = @seasonId")
+ .SetParameter("@seasonId", seasonId)
+ .Execute()
+ .Select(r => new SeasonLeaderboardReward
+ {
+ Id = r.GetValue("id"),
+ SeasonId = r.GetValue("season_id"),
+ RankMin = r.GetValue("rank_min"),
+ RankMax = r.GetValue("rank_max"),
+ PackageId = r.GetValue("package_id"),
+ })
+ .ToList();
+ }
+
+ // ── Point tracking ───────────────────────────────────────────────────
+
+ /// Atomically adds points and returns the new running total.
+ public long AddPoints(int characterId, int seasonId, long points)
+ {
+ return Db.Query()
+ .CommandText(@"
+ MERGE season_character_points WITH (HOLDLOCK) AS t
+ USING (SELECT @characterId AS character_id, @seasonId AS season_id) AS s
+ ON t.character_id = s.character_id AND t.season_id = s.season_id
+ WHEN MATCHED THEN
+ UPDATE SET total_points = total_points + @points,
+ last_updated = GETUTCDATE()
+ WHEN NOT MATCHED THEN
+ INSERT (character_id, season_id, total_points, last_updated,
+ intro_mail_sent, leaderboard_reward_delivered)
+ VALUES (@characterId, @seasonId, @points, GETUTCDATE(), 0, 0);
+
+ SELECT total_points FROM season_character_points
+ WHERE character_id = @characterId AND season_id = @seasonId;")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@points", points)
+ .ExecuteScalar();
+ }
+
+ // ── Objective progress ───────────────────────────────────────────────
+
+ ///
+ /// Increments objective progress. Returns (currentValue, bonusAwarded).
+ /// Does not increment if already completed.
+ ///
+ public (long currentValue, bool bonusAwarded) IncrementObjectiveProgress(
+ int characterId, int seasonId, int objectiveId, long amount)
+ {
+ var record = Db.Query()
+ .CommandText(@"
+ MERGE season_objective_progress WITH (HOLDLOCK) AS t
+ USING (SELECT @characterId AS character_id, @seasonId AS season_id,
+ @objectiveId AS objective_id) AS s
+ ON t.character_id = s.character_id
+ AND t.season_id = s.season_id
+ AND t.objective_id = s.objective_id
+ WHEN MATCHED AND t.completed = 0 THEN
+ UPDATE SET current_value = current_value + @amount
+ WHEN NOT MATCHED THEN
+ INSERT (character_id, season_id, objective_id, current_value,
+ completed, bonus_awarded)
+ VALUES (@characterId, @seasonId, @objectiveId, @amount, 0, 0);
+
+ SELECT current_value, bonus_awarded FROM season_objective_progress
+ WHERE character_id = @characterId
+ AND season_id = @seasonId
+ AND objective_id = @objectiveId;")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@objectiveId", objectiveId)
+ .SetParameter("@amount", amount)
+ .ExecuteSingleRow();
+
+ return (record.GetValue("current_value"),
+ record.GetValue("bonus_awarded"));
+ }
+
+ ///
+ /// Marks objective bonus as awarded. Returns true if this call was the first to do so
+ /// (false means another thread already awarded it).
+ ///
+ public bool MarkObjectiveBonusAwarded(int characterId, int seasonId, int objectiveId)
+ {
+ int rows = Db.Query()
+ .CommandText(@"
+ UPDATE season_objective_progress
+ SET bonus_awarded = 1,
+ completed = 1,
+ completed_time = GETUTCDATE()
+ WHERE character_id = @characterId
+ AND season_id = @seasonId
+ AND objective_id = @objectiveId
+ AND bonus_awarded = 0")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@objectiveId", objectiveId)
+ .ExecuteNonQuery();
+
+ return rows > 0;
+ }
+
+ // ── Tier claims ──────────────────────────────────────────────────────
+
+ public HashSet GetClaimedTierIds(int characterId, int seasonId)
+ {
+ return Db.Query()
+ .CommandText("SELECT tier_id FROM season_tier_claims " +
+ "WHERE character_id = @characterId AND season_id = @seasonId")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .Execute()
+ .Select(r => r.GetValue("tier_id"))
+ .ToHashSet();
+ }
+
+ /// Inserts a tier claim. Returns true if newly inserted, false if already existed.
+ public bool InsertTierClaim(int characterId, int seasonId, int tierId)
+ {
+ int rows = Db.Query()
+ .CommandText(@"
+ INSERT INTO season_tier_claims (character_id, season_id, tier_id, claimed_time)
+ SELECT @characterId, @seasonId, @tierId, GETUTCDATE()
+ WHERE NOT EXISTS (
+ SELECT 1 FROM season_tier_claims
+ WHERE character_id = @characterId
+ AND season_id = @seasonId
+ AND tier_id = @tierId)")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@tierId", tierId)
+ .ExecuteNonQuery();
+
+ return rows > 0;
+ }
+
+ // ── Package / reward delivery ────────────────────────────────────────
+
+ public List GetPackageItems(int packageId)
+ {
+ return Db.Query()
+ .CommandText("SELECT definition, quantity FROM packageitems WHERE packageid = @packageId")
+ .SetParameter("@packageId", packageId)
+ .Execute()
+ .Select(r => new SeasonPackageItem
+ {
+ Definition = r.GetValue("definition"),
+ Quantity = r.GetValue("quantity"),
+ })
+ .ToList();
+ }
+
+ public void InsertRedeemableItems(int accountId, int packageId, List items)
+ {
+ foreach (var item in items)
+ {
+ Db.Query()
+ .CommandText("INSERT INTO accountredeemableitems " +
+ "(accountid, definition, quantity, packageid) " +
+ "VALUES (@accountId, @definition, @quantity, @packageId)")
+ .SetParameter("@accountId", accountId)
+ .SetParameter("@definition", item.Definition)
+ .SetParameter("@quantity", item.Quantity)
+ .SetParameter("@packageId", packageId)
+ .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.SQLInsertError);
+ }
+ }
+
+ // ── End-of-season ────────────────────────────────────────────────────
+
+ public List GetParticipantRankings(int seasonId)
+ {
+ return Db.Query()
+ .CommandText(@"
+ SELECT character_id, season_id, total_points,
+ intro_mail_sent, leaderboard_reward_delivered
+ FROM season_character_points
+ WHERE season_id = @seasonId
+ ORDER BY total_points DESC")
+ .SetParameter("@seasonId", seasonId)
+ .Execute()
+ .Select(r => new SeasonCharacterPoints
+ {
+ CharacterId = r.GetValue("character_id"),
+ SeasonId = r.GetValue("season_id"),
+ TotalPoints = r.GetValue("total_points"),
+ IntroMailSent = r.GetValue("intro_mail_sent"),
+ LeaderboardRewardDelivered = r.GetValue("leaderboard_reward_delivered"),
+ })
+ .ToList();
+ }
+
+ public void MarkLeaderboardDelivered(int characterId, int seasonId)
+ {
+ Db.Query()
+ .CommandText("UPDATE season_character_points " +
+ "SET leaderboard_reward_delivered = 1 " +
+ "WHERE character_id = @characterId AND season_id = @seasonId")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .ExecuteNonQuery();
+ }
+
+ public void DeactivateSeason(int seasonId)
+ {
+ Db.Query()
+ .CommandText("UPDATE seasons SET is_active = 0 WHERE id = @id")
+ .SetParameter("@id", seasonId)
+ .ExecuteNonQuery();
+ }
+
+ // ── Intro mail tracking ──────────────────────────────────────────────
+
+ ///
+ /// Returns true if the intro mail flag was just set (first login this season).
+ /// Returns false if already sent.
+ ///
+ public bool TryMarkIntroMailSent(int characterId, int seasonId)
+ {
+ // Ensure row exists first (character may not have any points yet)
+ Db.Query()
+ .CommandText(@"
+ MERGE season_character_points WITH (HOLDLOCK) AS t
+ USING (SELECT @characterId AS character_id, @seasonId AS season_id) AS s
+ ON t.character_id = s.character_id AND t.season_id = s.season_id
+ WHEN NOT MATCHED THEN
+ INSERT (character_id, season_id, total_points, last_updated,
+ intro_mail_sent, leaderboard_reward_delivered)
+ VALUES (@characterId, @seasonId, 0, GETUTCDATE(), 0, 0);")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .ExecuteNonQuery();
+
+ int rows = Db.Query()
+ .CommandText("UPDATE season_character_points " +
+ "SET intro_mail_sent = 1 " +
+ "WHERE character_id = @characterId " +
+ " AND season_id = @seasonId " +
+ " AND intro_mail_sent = 0")
+ .SetParameter("@characterId", characterId)
+ .SetParameter("@seasonId", seasonId)
+ .ExecuteNonQuery();
+
+ return rows > 0;
+ }
+
+ // ── Admin commands ───────────────────────────────────────────────────
+
+ public int CreateSeason(string name, string description, DateTime start, DateTime end)
+ {
+ return Db.Query()
+ .CommandText("INSERT INTO seasons (name, description, start_time, end_time, is_active) " +
+ "VALUES (@name, @description, @start, @end, 0); " +
+ "SELECT CAST(SCOPE_IDENTITY() AS INT)")
+ .SetParameter("@name", name)
+ .SetParameter("@description", description)
+ .SetParameter("@start", start)
+ .SetParameter("@end", end)
+ .ExecuteScalar().ThrowIfEqual(0, ErrorCodes.SQLInsertError);
+ }
+
+ public void SetSeasonActive(int seasonId, bool active)
+ {
+ Db.Query()
+ .CommandText("UPDATE seasons SET is_active = @active WHERE id = @id")
+ .SetParameter("@active", active ? 1 : 0)
+ .SetParameter("@id", seasonId)
+ .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.ItemNotFound);
+ }
+
+ public void AddActivityRate(int seasonId, SeasonActivityType type, double ptsPerUnit, int scale)
+ {
+ Db.Query()
+ .CommandText("INSERT INTO season_activity_rates " +
+ "(season_id, activity_type, points_per_unit, unit_scale) " +
+ "VALUES (@seasonId, @type, @pts, @scale)")
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@type", (int)type)
+ .SetParameter("@pts", ptsPerUnit)
+ .SetParameter("@scale", scale)
+ .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.SQLInsertError);
+ }
+
+ public void AddObjective(int seasonId, SeasonActivityType type, long target,
+ int bonusPts, string name, string description)
+ {
+ Db.Query()
+ .CommandText("INSERT INTO season_objectives " +
+ "(season_id, activity_type, target_value, bonus_points, name, description) " +
+ "VALUES (@seasonId, @type, @target, @bonus, @name, @desc)")
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@type", (int)type)
+ .SetParameter("@target", target)
+ .SetParameter("@bonus", bonusPts)
+ .SetParameter("@name", name)
+ .SetParameter("@desc", description)
+ .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.SQLInsertError);
+ }
+
+ public void AddTier(int seasonId, int tierNum, string tierName, int ptsRequired, int packageId)
+ {
+ Db.Query()
+ .CommandText("INSERT INTO season_tiers " +
+ "(season_id, tier_number, tier_name, points_required, package_id) " +
+ "VALUES (@seasonId, @num, @name, @pts, @pkg)")
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@num", tierNum)
+ .SetParameter("@name", tierName)
+ .SetParameter("@pts", ptsRequired)
+ .SetParameter("@pkg", packageId)
+ .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.SQLInsertError);
+ }
+
+ public void AddLeaderboardReward(int seasonId, int rankMin, int rankMax, int packageId)
+ {
+ Db.Query()
+ .CommandText("INSERT INTO season_leaderboard_rewards " +
+ "(season_id, rank_min, rank_max, package_id) " +
+ "VALUES (@seasonId, @min, @max, @pkg)")
+ .SetParameter("@seasonId", seasonId)
+ .SetParameter("@min", rankMin)
+ .SetParameter("@max", rankMax)
+ .SetParameter("@pkg", packageId)
+ .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.SQLInsertError);
+ }
+
+ public (string name, TimeSpan remaining, int participants) GetSeasonStatus()
+ {
+ var record = Db.Query()
+ .CommandText("SELECT s.name, s.end_time, " +
+ "(SELECT COUNT(*) FROM season_character_points p WHERE p.season_id = s.id) AS cnt " +
+ "FROM seasons s WHERE s.is_active = 1")
+ .ExecuteSingleRow();
+
+ if (record == null)
+ return ("(none)", TimeSpan.Zero, 0);
+
+ var endTime = record.GetValue("end_time");
+ return (record.GetValue("name"),
+ endTime - DateTime.UtcNow,
+ record.GetValue("cnt"));
+ }
+
+ public Season? GetSeasonById(int seasonId)
+ {
+ var record = Db.Query()
+ .CommandText("SELECT id, name, description, start_time, end_time, is_active " +
+ "FROM seasons WHERE id = @id")
+ .SetParameter("@id", seasonId)
+ .ExecuteSingleRow();
+
+ if (record == null) return null;
+
+ return new Season
+ {
+ Id = record.GetValue("id"),
+ Name = record.GetValue("name"),
+ Description = record.GetValue("description"),
+ StartTime = record.GetValue("start_time"),
+ EndTime = record.GetValue("end_time"),
+ IsActive = record.GetValue("is_active"),
+ };
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Expected: build succeeds.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonRepository.cs
+git commit -m "feat(seasons): add SeasonRepository with all DB operations"
+```
+
+---
+
+## Task 4: SeasonServiceLocator
+
+**Files:**
+- Create: `src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs`
+
+This static holder lets hook sites in non-Autofac-managed classes (CharacterWallet, Modules) call `RecordActivity` without constructor injection.
+
+- [ ] **Step 1: Create `SeasonServiceLocator.cs`**
+
+```csharp
+namespace Perpetuum.Services.Seasons
+{
+ public static class SeasonServiceLocator
+ {
+ public static ISeasonService? Instance { get; set; }
+ }
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs
+git commit -m "feat(seasons): add SeasonServiceLocator static accessor"
+```
+
+---
+
+## Task 5: SeasonService
+
+**Files:**
+- Create: `src/Perpetuum/Services/Seasons/SeasonService.cs`
+
+The service extends `Process` (so the process manager calls `Update` on a timer). It caches the active season in immutable fields swapped atomically. `RecordActivity` is synchronous and safe to call from any thread.
+
+- [ ] **Step 1: Create `SeasonService.cs`**
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Perpetuum.Accounting.Characters;
+using Perpetuum.Services.Mail;
+using Perpetuum.Sessions;
+using Perpetuum.Threading.Process;
+
+namespace Perpetuum.Services.Seasons
+{
+ public class SeasonService : Process, ISeasonService
+ {
+ private static readonly TimeSpan CacheRefreshInterval = TimeSpan.FromMinutes(5);
+
+ private readonly SeasonRepository _repository;
+ private readonly ISessionManager _sessionManager;
+
+ // Immutable snapshot replaced atomically on refresh.
+ private volatile Season? _activeSeason;
+ private ImmutableList _activeRates = ImmutableList.Empty;
+ private ImmutableList _activeObjectives = ImmutableList.Empty;
+ private ImmutableList _activeTiers = ImmutableList.Empty;
+ private ImmutableList _activeLeaderboard = ImmutableList.Empty;
+
+ private TimeSpan _cacheAge = CacheRefreshInterval; // trigger load immediately on first Update
+
+ public SeasonService(SeasonRepository repository, ISessionManager sessionManager)
+ {
+ _repository = repository;
+ _sessionManager = sessionManager;
+ }
+
+ // ── Process loop ─────────────────────────────────────────────────────
+
+ public override void Update(TimeSpan time)
+ {
+ _cacheAge += time;
+ if (_cacheAge >= CacheRefreshInterval)
+ {
+ _cacheAge = TimeSpan.Zero;
+ RefreshCache();
+ }
+
+ var season = _activeSeason;
+ if (season != null && DateTime.UtcNow > season.EndTime)
+ ProcessSeasonEnd(season);
+ }
+
+ private void RefreshCache()
+ {
+ var season = _repository.GetActiveSeason();
+ if (season == null)
+ {
+ _activeSeason = null;
+ _activeRates = ImmutableList.Empty;
+ _activeObjectives = ImmutableList.Empty;
+ _activeTiers = ImmutableList.Empty;
+ _activeLeaderboard = ImmutableList.Empty;
+ return;
+ }
+
+ _activeRates = _repository.GetActivityRates(season.Id).ToImmutableList();
+ _activeObjectives = _repository.GetObjectives(season.Id).ToImmutableList();
+ _activeTiers = _repository.GetTiers(season.Id).ToImmutableList();
+ _activeLeaderboard = _repository.GetLeaderboardRewards(season.Id).ToImmutableList();
+ _activeSeason = season; // assign last so other threads see a consistent snapshot
+ }
+
+ // ── ISeasonService ────────────────────────────────────────────────────
+
+ public void RecordActivity(int characterId, SeasonActivityType activityType, long amount)
+ {
+ var season = _activeSeason;
+ if (season == null || DateTime.UtcNow > season.EndTime)
+ return;
+
+ var rates = _activeRates.Where(r => r.ActivityType == activityType).ToList();
+ if (rates.Count == 0)
+ return;
+
+ long basePoints = 0;
+ foreach (var rate in rates)
+ {
+ long scale = rate.UnitScale > 0 ? rate.UnitScale : 1;
+ basePoints += (long)Math.Floor((double)amount / scale * rate.PointsPerUnit);
+ }
+
+ if (basePoints <= 0)
+ return;
+
+ long newTotal = _repository.AddPoints(characterId, season.Id, basePoints);
+
+ // Objective progress
+ foreach (var obj in _activeObjectives.Where(o => o.ActivityType == activityType))
+ {
+ var (currentValue, bonusAwarded) =
+ _repository.IncrementObjectiveProgress(characterId, season.Id, obj.Id, amount);
+
+ if (!bonusAwarded && currentValue >= obj.TargetValue)
+ {
+ if (_repository.MarkObjectiveBonusAwarded(characterId, season.Id, obj.Id))
+ {
+ newTotal = _repository.AddPoints(characterId, season.Id, obj.BonusPoints);
+ SendObjectiveCompleteMail(characterId, obj, newTotal);
+ }
+ }
+ }
+
+ // Tier crossings
+ var claimed = _repository.GetClaimedTierIds(characterId, season.Id);
+ foreach (var tier in _activeTiers.Where(t => t.PointsRequired <= newTotal && !claimed.Contains(t.Id))
+ .OrderBy(t => t.TierNumber))
+ {
+ if (_repository.InsertTierClaim(characterId, season.Id, tier.Id))
+ DeliverTierReward(characterId, season.Id, tier, newTotal);
+ }
+ }
+
+ public void OnCharacterLogin(Character character)
+ {
+ var season = _activeSeason;
+ if (season == null || DateTime.UtcNow > season.EndTime)
+ return;
+
+ if (_repository.TryMarkIntroMailSent(character.Id, season.Id))
+ SendIntroMail(character, season);
+ }
+
+ // ── Reward delivery ──────────────────────────────────────────────────
+
+ private void DeliverTierReward(int characterId, int seasonId, SeasonTier tier, long currentPoints)
+ {
+ var items = _repository.GetPackageItems(tier.PackageId);
+ if (items.Count == 0)
+ return;
+
+ var character = Character.Get(characterId);
+ _repository.InsertRedeemableItems(character.AccountId, tier.PackageId, items);
+ SendTierUnlockMail(characterId, tier, currentPoints);
+ }
+
+ private void DeliverLeaderboardReward(int characterId, SeasonLeaderboardReward reward)
+ {
+ var items = _repository.GetPackageItems(reward.PackageId);
+ if (items.Count == 0)
+ return;
+
+ var character = Character.Get(characterId);
+ _repository.InsertRedeemableItems(character.AccountId, reward.PackageId, items);
+ }
+
+ // ── End-of-season ────────────────────────────────────────────────────
+
+ private void ProcessSeasonEnd(Season season)
+ {
+ // Guard: only one thread processes end-of-season
+ _activeSeason = null;
+ _repository.DeactivateSeason(season.Id);
+
+ var rankings = _repository.GetParticipantRankings(season.Id);
+ var leaderboard = _activeLeaderboard;
+
+ for (int rank = 1; rank <= rankings.Count; rank++)
+ {
+ var entry = rankings[rank - 1];
+ if (entry.LeaderboardRewardDelivered)
+ continue;
+
+ var reward = leaderboard.FirstOrDefault(r => rank >= r.RankMin && rank <= r.RankMax);
+ if (reward != null)
+ DeliverLeaderboardReward(entry.CharacterId, reward);
+
+ _repository.MarkLeaderboardDelivered(entry.CharacterId, season.Id);
+ SendFinalStandingsMail(entry.CharacterId, rank, entry.TotalPoints,
+ reward != null, season.Name);
+ }
+
+ // Clear cache
+ _activeRates = ImmutableList.Empty;
+ _activeObjectives = ImmutableList.Empty;
+ _activeTiers = ImmutableList.Empty;
+ _activeLeaderboard = ImmutableList.Empty;
+ }
+
+ // ── Mail helpers ─────────────────────────────────────────────────────
+
+ private static void SendIntroMail(Character character, Season season)
+ {
+ string subject = $"Season Active: {season.Name}";
+ string body = $"{season.Description}\n\nSeason ends: {season.EndTime:yyyy-MM-dd HH:mm} UTC";
+ MailHandler.SendMail(Character.None, character, subject, body,
+ MailType.character, out _, out _);
+ }
+
+ private static void SendObjectiveCompleteMail(int characterId, SeasonObjective obj, long total)
+ {
+ var character = Character.Get(characterId);
+ string subject = $"Objective Complete: {obj.Name}";
+ string body = $"You completed the objective '{obj.Name}' and earned {obj.BonusPoints} bonus points.\nTotal season points: {total}";
+ MailHandler.SendMail(Character.None, character, subject, body,
+ MailType.character, out _, out _);
+ }
+
+ private static void SendTierUnlockMail(int characterId, SeasonTier tier, long total)
+ {
+ var character = Character.Get(characterId);
+ string subject = $"Tier Unlocked: {tier.TierName}";
+ string body = $"You reached {tier.PointsRequired} season points and unlocked the {tier.TierName} tier reward!\nTotal points: {total}\nRedeem your reward at any terminal via the Redeemable Items menu.";
+ MailHandler.SendMail(Character.None, character, subject, body,
+ MailType.character, out _, out _);
+ }
+
+ private static void SendFinalStandingsMail(int characterId, int rank, long total,
+ bool hasLeaderboardReward, string seasonName)
+ {
+ var character = Character.Get(characterId);
+ string subject = $"Season Ended: {seasonName}";
+ string body = $"The season has ended.\n\nYour final rank: #{rank}\nTotal points: {total}";
+ if (hasLeaderboardReward)
+ body += "\n\nYou earned a leaderboard reward! Redeem it at any terminal.";
+ MailHandler.SendMail(Character.None, character, subject, body,
+ MailType.character, out _, out _);
+ }
+
+ public void SendActivationMailToOnlineCharacters(Season season)
+ {
+ foreach (var session in _sessionManager.Sessions)
+ {
+ var character = session.Character;
+ if (character == null || character == Character.None)
+ continue;
+
+ SendIntroMail(character, season);
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+If `ISessionManager.Sessions` doesn't exist exactly as shown, search the codebase:
+```
+grep -r "ISessionManager" src/Perpetuum/Sessions/ --include="*.cs" -l
+```
+Then open the interface file and use the correct property/method to enumerate active sessions.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add src/Perpetuum/Services/Seasons/SeasonService.cs
+git commit -m "feat(seasons): add SeasonService with cache, RecordActivity, end-of-season processing"
+```
+
+---
+
+## Task 6: SeasonModule + Bootstrapper Registration
+
+**Files:**
+- Create: `src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs`
+- Modify: `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs`
+
+- [ ] **Step 1: Create `SeasonModule.cs`**
+
+Follow the pattern from `MissionsModule.cs`. The service is registered as `SingleInstance`, added to `IProcessManager` with a 1-minute tick, and sets the static locator.
+
+```csharp
+using Autofac;
+using Perpetuum.Services.Seasons;
+using Perpetuum.Threading.Process;
+
+namespace Perpetuum.Bootstrapper.Modules
+{
+ internal class SeasonModule : Module
+ {
+ protected override void Load(ContainerBuilder builder)
+ {
+ builder.RegisterType().SingleInstance();
+
+ builder.RegisterType()
+ .As()
+ .OnActivated(e =>
+ {
+ SeasonServiceLocator.Instance = e.Instance;
+ var pm = e.Context.Resolve();
+ pm.AddProcess(e.Instance.ToAsync().AsTimed(TimeSpan.FromMinutes(1)));
+ })
+ .SingleInstance();
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Register SeasonModule in `PerpetuumBootstrapper.cs`**
+
+Open `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs` and find where other modules are registered (e.g., `RegisterModule()`). Add:
+
+```csharp
+RegisterModule();
+```
+
+Place it near the other service modules.
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs
+git add src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs
+git commit -m "feat(seasons): register SeasonModule in bootstrapper"
+```
+
+---
+
+## Task 7: Hook — NPC Kill
+
+**Files:**
+- Modify: `src/Perpetuum/Zones/NpcSystem/Npc.cs`
+
+The hook goes inside `HandleNpcDead` after the killer player is resolved. At that point `killerPlayer` is a `Player` with a `.Character` property.
+
+- [ ] **Step 1: Find the exact location**
+
+Open `src/Perpetuum/Zones/NpcSystem/Npc.cs`. Search for `HandleNpcDead`. Find the line:
+```csharp
+Player killerPlayer = zone.ToPlayerOrGetOwnerPlayer(killer);
+```
+
+- [ ] **Step 2: Add the hook after the null check**
+
+Immediately after the `if (killerPlayer != null)` block (the one that calls `EnqueueKill`), add:
+
+```csharp
+if (killerPlayer != null)
+{
+ EnqueueKill(killerPlayer, killer);
+ SeasonServiceLocator.Instance?.RecordActivity(
+ killerPlayer.Character.Id, SeasonActivityType.NpcKill, 1);
+}
+```
+
+Add the using directive at the top of the file if not present:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Zones/NpcSystem/Npc.cs
+git commit -m "feat(seasons): add NPC kill hook to SeasonService"
+```
+
+---
+
+## Task 8: Hook — PvP Kill
+
+**Files:**
+- Modify: `src/Perpetuum/Players/Player.cs`
+
+The hook goes in `HandlePlayerDead` after the killer is resolved via `zone.ToPlayerOrGetOwnerPlayer`. Only fire when the killer is a different player (not self-kill from fall damage etc.).
+
+- [ ] **Step 1: Find the exact location**
+
+Open `src/Perpetuum/Players/Player.cs`. Find `HandlePlayerDead`. Find:
+```csharp
+killer = zone.ToPlayerOrGetOwnerPlayer(killer) ?? killer;
+SaveCombatLog(zone, killer);
+```
+
+- [ ] **Step 2: Add the hook after `SaveCombatLog`**
+
+```csharp
+killer = zone.ToPlayerOrGetOwnerPlayer(killer) ?? killer;
+SaveCombatLog(zone, killer);
+
+if (killer is Player killerPlayer && killerPlayer != this)
+{
+ SeasonServiceLocator.Instance?.RecordActivity(
+ killerPlayer.Character.Id, SeasonActivityType.PvpKill, 1);
+}
+```
+
+Add the using directive:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Players/Player.cs
+git commit -m "feat(seasons): add PvP kill hook + character login hook to SeasonService"
+```
+
+---
+
+## Task 9: Hook — Character Login (Intro Mail)
+
+**Files:**
+- Modify: `src/Perpetuum/Players/Player.cs`
+
+The login hook sends the intro mail the first time a character logs in during an active season. Find where a player enters the game world (zone entry or session login).
+
+- [ ] **Step 1: Find the login/zone-entry event in `Player.cs`**
+
+Search `Player.cs` for `AddToZone`, `OnAddedToZone`, `OnEnterZone`, or `EnterZone`. This is where the player "arrives" and is the right place to trigger the intro mail.
+
+- [ ] **Step 2: Add the login hook**
+
+Inside the zone-entry method, add after the player has been added:
+
+```csharp
+SeasonServiceLocator.Instance?.OnCharacterLogin(Character);
+```
+
+Add the using directive if not already present:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Players/Player.cs
+git commit -m "feat(seasons): add player login hook for season intro mail"
+```
+
+---
+
+## Task 10: Hook — Mission Complete
+
+**Files:**
+- Modify: `src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs`
+
+`TryFinishMission` returns `null` for incomplete missions. When it returns a non-null list, the mission is done. Award each participant 1 `MissionComplete` point.
+
+- [ ] **Step 1: Find the exact location**
+
+Open the file. Find `TryFinishMission`. Inside the `lock (LockObject)` block, locate:
+```csharp
+missionInProgress.SetSuccessToMissionLog(true);
+```
+
+The `participants` variable (a list of `Character?`) is in scope at this point.
+
+- [ ] **Step 2: Add the hook after `SetSuccessToMissionLog`**
+
+```csharp
+missionInProgress.SetSuccessToMissionLog(true);
+
+foreach (var participant in participants)
+{
+ if (participant != null)
+ {
+ SeasonServiceLocator.Instance?.RecordActivity(
+ participant.Id, SeasonActivityType.MissionComplete, 1);
+ }
+}
+```
+
+Add the using directive:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs
+git commit -m "feat(seasons): add mission complete hook to SeasonService"
+```
+
+---
+
+## Task 11: Hook — NIC Earned and Spent
+
+**Files:**
+- Modify: `src/Perpetuum/Accounting/Characters/CharacterWallet.cs`
+
+`OnCommited` fires after every wallet transaction. The `change` variable is positive for credits (NIC earned) and negative for debits (NIC spent).
+
+- [ ] **Step 1: Find `OnCommited` in `CharacterWallet.cs`**
+
+Open the file. Find `protected override void OnCommited(double startBalance)`. The method calculates `change = currentCredit - startBalance`.
+
+- [ ] **Step 2: Add NIC hooks at the end of `OnCommited`**
+
+Add after the `Message.Builder...Send()` block:
+
+```csharp
+if (change > 0)
+{
+ SeasonServiceLocator.Instance?.RecordActivity(
+ character.Id, SeasonActivityType.NicEarned, (long)change);
+}
+else if (change < 0)
+{
+ SeasonServiceLocator.Instance?.RecordActivity(
+ character.Id, SeasonActivityType.NicSpent, (long)Math.Abs(change));
+}
+```
+
+Add the using directive:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+Note: `CharacterWallet` handles all wallet types. If this causes unexpected season points from internal transfers, add a filter: check `transactionType` against a known "player NIC" set. The existing `TransactionType` enum values are defined elsewhere; search for `TransactionType.` usages in context to identify which types represent direct player NIC gains/losses.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Accounting/Characters/CharacterWallet.cs
+git commit -m "feat(seasons): add NIC earned/spent hooks to SeasonService"
+```
+
+---
+
+## Task 12: Hook — EP Spent
+
+**Files:**
+- Modify: `src/Perpetuum/Accounting/AccountManager.cs`
+
+`AddExtensionPointsSpent` is called exactly when a player spends EP on an extension. `spentPoints` is the amount.
+
+- [ ] **Step 1: Find `AddExtensionPointsSpent` in `AccountManager.cs`**
+
+Open the file. Find `public void AddExtensionPointsSpent(Account account, Character character, int spentPoints, int extensionID, int extensionLevel)`.
+
+- [ ] **Step 2: Add the hook at the end of the method**
+
+After the `Db.Query()...ExecuteNonQuery()` call, add:
+
+```csharp
+SeasonServiceLocator.Instance?.RecordActivity(
+ character.Id, SeasonActivityType.EpSpent, spentPoints);
+```
+
+Add the using directive:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Accounting/AccountManager.cs
+git commit -m "feat(seasons): add EP spent hook to SeasonService"
+```
+
+---
+
+## Task 13: Hook — Intrusion / SAP
+
+**Files:**
+- Modify: `src/Perpetuum/Zones/Intrusion/Outpost.cs`
+
+`ProcessStabilityChange` awards EP to each participant in `sap.Participants`. Hook into that loop.
+
+- [ ] **Step 1: Find the participant EP loop in `Outpost.cs`**
+
+Open the file. Find `ProcessStabilityChange`. Locate the loop:
+```csharp
+foreach (Players.Player player in sap.Participants)
+{
+ player.Character.AddExtensionPointsBoostAndLog(EpForActivityType.Intrusion, EP_WINNER);
+}
+```
+
+- [ ] **Step 2: Add the hook inside the loop**
+
+```csharp
+foreach (Players.Player player in sap.Participants)
+{
+ player.Character.AddExtensionPointsBoostAndLog(EpForActivityType.Intrusion, EP_WINNER);
+ SeasonServiceLocator.Instance?.RecordActivity(
+ player.Character.Id, SeasonActivityType.IntrusionPoint, 1);
+}
+```
+
+Add the using directive:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Zones/Intrusion/Outpost.cs
+git commit -m "feat(seasons): add intrusion/SAP hook to SeasonService"
+```
+
+---
+
+## Task 14: Hook — Mineral Mined (Driller + LargeDriller)
+
+**Files:**
+- Modify: `src/Perpetuum/Modules/DrillerModule.cs`
+- Modify: `src/Perpetuum/Modules/LargeDrillerModule.cs`
+
+Both driller modules extract minerals in a `foreach (ItemInfo material in extractedMaterials)` loop. Hook into both.
+
+- [ ] **Step 1: Add hook in `DrillerModule.cs`**
+
+Open `src/Perpetuum/Modules/DrillerModule.cs`. Find `DoExtractMinerals`. Inside the `foreach (ItemInfo material in extractedMaterials)` loop, after `player.Zone?.MiningLogHandler.EnqueueMiningLog(...)`, add:
+
+```csharp
+SeasonServiceLocator.Instance?.RecordActivity(
+ player.Character.Id, SeasonActivityType.MineralMined, material.Quantity);
+```
+
+Add the using directive:
+```csharp
+using Perpetuum.Services.Seasons;
+```
+
+- [ ] **Step 2: Add the same hook in `LargeDrillerModule.cs`**
+
+Open `src/Perpetuum/Modules/LargeDrillerModule.cs`. Find the equivalent mineral extraction loop. Add the identical hook:
+
+```csharp
+SeasonServiceLocator.Instance?.RecordActivity(
+ player.Character.Id, SeasonActivityType.MineralMined, material.Quantity);
+```
+
+Add the using directive if not present.
+
+- [ ] **Step 3: Build to verify**
+
+```bash
+dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/Perpetuum/Modules/DrillerModule.cs
+git add src/Perpetuum/Modules/LargeDrillerModule.cs
+git commit -m "feat(seasons): add mineral mined hooks to SeasonService"
+```
+
+---
+
+## Task 15: Admin Command Handlers
+
+**Files:**
+- Create: `src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs`
+
+All commands follow the pattern in `AdminCommandHandlers.cs`: static methods with `[ChatCommand("name")]`, `AdminCommandData data` parameter, `data.Command.Args[]` for arguments, `SendMessageToAll(data, msg)` for replies.
+
+Admin commands write directly to the DB; `SeasonService` picks up the change on its next cache refresh (within 5 minutes). For `SeasonActivate`, after the DB write the handler also triggers an immediate cache refresh and sends activation mails by resolving `ISeasonService` — however, since handlers are static, the static locator is used.
+
+- [ ] **Step 1: Create `SeasonAdminCommandHandlers.cs`**
+
+```csharp
+using System;
+using System.Globalization;
+using Perpetuum.Services.Channels.ChatCommands;
+using Perpetuum.Services.Seasons;
+
+namespace Perpetuum.Services.Channels.ChatCommands
+{
+ public static class SeasonAdminCommandHandlers
+ {
+ // #SeasonCreate
+ // Example: #SeasonCreate Summer2026 2026-06-01 2026-07-01
+ [ChatCommand("SeasonCreate")]
+ public static void SeasonCreate(AdminCommandData data)
+ {
+ CheckRequiredArgLength(data, 3);
+ string name = data.Command.Args[0];
+ if (!DateTime.TryParseExact(data.Command.Args[1], "yyyy-MM-dd",
+ CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime start) ||
+ !DateTime.TryParseExact(data.Command.Args[2], "yyyy-MM-dd",
+ CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime end))
+ {
+ SendMessageToAll(data, "Usage: #SeasonCreate ");
+ return;
+ }
+
+ var repo = new SeasonRepository();
+ int id = repo.CreateSeason(name, "", start, end);
+ SendMessageToAll(data, $"Season created. ID={id} Name='{name}' {start:yyyy-MM-dd} to {end:yyyy-MM-dd}. Use #SeasonAddRate, #SeasonAddTier, etc., then #SeasonActivate {id}.");
+ }
+
+ // #SeasonActivate
+ [ChatCommand("SeasonActivate")]
+ public static void SeasonActivate(AdminCommandData data)
+ {
+ CheckRequiredArgLength(data, 1);
+ if (!int.TryParse(data.Command.Args[0], out int id))
+ {
+ SendMessageToAll(data, "Usage: #SeasonActivate ");
+ return;
+ }
+
+ var repo = new SeasonRepository();
+ repo.SetSeasonActive(id, true);
+
+ // Trigger immediate cache refresh and send activation mails
+ if (SeasonServiceLocator.Instance is SeasonService svc)
+ {
+ // Force refresh on next Update tick by resetting; simplest is just to let the
+ // 5-min timer pick it up. For immediate effect call internal method if accessible,
+ // otherwise this will activate within 5 minutes automatically.
+ var season = repo.GetSeasonById(id);
+ if (season != null)
+ svc.SendActivationMailToOnlineCharacters(season);
+ }
+
+ SendMessageToAll(data, $"Season {id} activated. Players will receive intro mails shortly.");
+ }
+
+ // #SeasonDeactivate