Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
23cdc54
Add design spec for IMPROVEMENT-033: Equipment Set Rewards for Seasons
Sellafield May 30, 2026
afc6dd3
Add implementation plan for IMPROVEMENT-033: Equipment Set Season Rew…
Sellafield May 30, 2026
c243e47
feat(seasons): add EquipmentSetId to reward models; PackageId nullabl…
Sellafield May 30, 2026
76deae9
feat(seasons): read equipment_set_id in GetTiers/GetObjectives/GetLea…
Sellafield May 30, 2026
dd786d5
feat(seasons): add GetSetMemberDefinitions and InsertRedeemableItem t…
Sellafield May 30, 2026
94712d7
feat(seasons): delivery branching for equipment set rewards in tiers,…
Sellafield May 30, 2026
98ffc24
fix(seasons): use Random.Shared for set reward selection; remove unus…
Sellafield May 30, 2026
3b11dec
feat(admintool/seasons): add EquipmentSetId and SelectedEquipmentSet …
Sellafield May 30, 2026
bf29419
feat(admintool/seasons): add equipment_set_id to all six SeasonChange…
Sellafield May 30, 2026
59949e9
feat(admintool/seasons): read equipment_set_id in load methods; add L…
Sellafield May 30, 2026
12ecd1f
feat(admintool/seasons): EquipmentSets list, SelectedEquipmentSet wir…
Sellafield May 30, 2026
d118f4d
feat(admintool/seasons): add Equipment Set reward columns and Leaderb…
Sellafield May 30, 2026
2b85965
docs: add p36.5 migration for IMPROVEMENT-033 equipment set season re…
Sellafield May 30, 2026
2c6ad11
docs(backlog): mark IMPROVEMENT-033 as DONE
Sellafield May 30, 2026
4da1c51
docs(spec): add IMPROVEMENT-032 export SQL scripts design
Sellafield Jun 2, 2026
e201021
docs(plan): add IMPROVEMENT-032 export SQL scripts implementation plan
Sellafield Jun 2, 2026
76772c7
feat(admintool/export): SqlExportBuilder helpers
Sellafield Jun 2, 2026
2f96ce7
feat(admintool/export): ItemExporter — full 11-section item chain
Sellafield Jun 2, 2026
e8f37dd
chore(admintool/export): clarify single-row read in AddItemResearchLe…
Sellafield Jun 2, 2026
e3a4a05
feat(admintool/export): SeasonExporter with full dependency traversal
Sellafield Jun 2, 2026
7f1eaef
fix(admintool/export): early return when season not found in SeasonEx…
Sellafield Jun 2, 2026
d7f6930
feat(admintool/export): RobotExporter with GenXY-aware description re…
Sellafield Jun 2, 2026
98dd988
chore(admintool/export): remove unused using in RobotExporter
Sellafield Jun 2, 2026
c363f48
feat(admintool/export): ExportScriptViewModel and ExportScriptWindow
Sellafield Jun 2, 2026
b1cf292
feat(admintool/export): Export SQL button in SeasonDetailView
Sellafield Jun 2, 2026
2814bc8
feat(admintool/export): Export SQL button in EntitiesView
Sellafield Jun 2, 2026
aa6143c
feat(admintool/export): Export SQL button in RobotTemplatesView
Sellafield Jun 2, 2026
4469c09
fix(admintool/export): fix FK ordering in SeasonExporter and orphaned…
Sellafield Jun 2, 2026
5a9d2f4
docs(backlog): mark IMPROVEMENT-032 as DONE
Sellafield Jun 3, 2026
d68950c
backlog
Sellafield Jun 3, 2026
d6baae3
docs(backlog): update ISSUE-025 with investigation findings and fix plan
Sellafield Jun 3, 2026
229f002
docs(plan): add ISSUE-025 leaderboard re-delivery implementation plan
Sellafield Jun 3, 2026
8379eee
feat(seasons): add RedeliverLeaderboardRewards for post-hoc reward re…
Sellafield Jun 3, 2026
cdf9ab1
fix(seasons): only mark leaderboard delivered when reward was actuall…
Sellafield Jun 3, 2026
87be071
feat(seasons): add #SeasonRedeliverLeaderboard chat command
Sellafield Jun 3, 2026
73e1d4a
fix(admintool/seasons): validate rank_min <= rank_max before queuing …
Sellafield Jun 3, 2026
099750e
fix(seasons): DeliverLeaderboardReward returns bool; gate delivery co…
Sellafield Jun 3, 2026
81bb03d
docs: add IMPROVEMENT-034 economy NIC flow design spec
Sellafield Jun 3, 2026
a34af8c
docs: add IMPROVEMENT-034 economy NIC flow implementation plan
Sellafield Jun 3, 2026
9a3a1b2
feat(economy): add EconomyNicFlowRow and EconomyRepository
Sellafield Jun 3, 2026
70580f6
feat(economy): add EconomyViewModel
Sellafield Jun 3, 2026
3fe9430
feat(economy): add EconomyView and LongToForegroundConverter
Sellafield Jun 3, 2026
1b70213
feat(economy): wire Economy panel into MainViewModel and MainWindow
Sellafield Jun 3, 2026
a147494
fix(economy): reclassify MissionTax(29) and CharacterCreate(36) as NI…
Sellafield Jun 3, 2026
cced226
docs(backlog): mark IMPROVEMENT-034 DONE
Sellafield Jun 3, 2026
bfd4b84
docs(spec): add IMPROVEMENT-039 economy health statistics design
Sellafield Jun 3, 2026
5d7a683
docs(plan): add IMPROVEMENT-039 economy health stats implementation plan
Sellafield Jun 3, 2026
930c727
feat(db): add economy_daily_snapshot and economy_price_index_basket t…
Sellafield Jun 3, 2026
7c3c2ed
feat(server): add EconomySnapshotService daily NIC snapshot job (IMPR…
Sellafield Jun 3, 2026
d39d64b
feat(admintool): extract EconomyNicFlowViewModel and EconomyNicFlowVi…
Sellafield Jun 3, 2026
8f369f7
feat(admintool): add Money Supply & Wealth tab (IMPROVEMENT-039)
Sellafield Jun 3, 2026
25103ce
fix(admintool): revert out-of-scope MainViewModel/MainWindow changes …
Sellafield Jun 3, 2026
25dc864
fix(admintool): fix top-1% denominator, idle NIC null handling, media…
Sellafield Jun 3, 2026
a05cf81
feat(admintool): add Market Health tab with price index basket config…
Sellafield Jun 3, 2026
eba4b00
feat(admintool): add Sink Effectiveness tab (IMPROVEMENT-039)
Sellafield Jun 3, 2026
f9a5cc1
feat(admintool): wire Economy panel into 4-tab layout (IMPROVEMENT-039)
Sellafield Jun 3, 2026
74b5744
docs(backlog): mark IMPROVEMENT-039 DONE
Sellafield Jun 3, 2026
17cc4af
fix(market): allow corp-only orders to auto-match same-corp counterpa…
Sellafield Jun 5, 2026
cdf439e
fix(automarket): prevent deleted trade list items from re-acquiring b…
Sellafield Jun 6, 2026
59df047
docs(insurance): add design spec for IMPROVEMENT-036 insurance overhaul
Sellafield Jun 6, 2026
c49e431
docs(insurance): add implementation plan for IMPROVEMENT-036 insuranc…
Sellafield Jun 6, 2026
36cf271
feat(insurance): add migration SQL for insurance_config and usp_Recal…
Sellafield Jun 6, 2026
1357ff4
refactor(insurance): remove unused InsuranceFeeMultiplier and Insuran…
Sellafield Jun 6, 2026
2fbb3ab
fix(insurance): wire GetFeeExtensionBonus into fee calculation at pur…
Sellafield Jun 6, 2026
2525d65
fix(insurance): apply fee extension bonus in InsuranceQuery so quoted…
Sellafield Jun 6, 2026
8f20422
feat(insurance): add InsurancePriceRefreshService — daily SP trigger …
Sellafield Jun 6, 2026
61ab786
feat(insurance): register InsurancePriceRefreshService in Autofac (IM…
Sellafield Jun 6, 2026
110d0b0
feat(insurance): add Admin Tool data types and EconomyInsuranceReposi…
Sellafield Jun 6, 2026
3977bd2
feat(insurance): add Insurance tab to Economy panel in Admin Tool (IM…
Sellafield Jun 6, 2026
b096751
fix(insurance): auto-load Insurance tab on first activation (IMPROVEM…
Sellafield Jun 6, 2026
d30ffa8
fix(insurance): code quality fixes in Insurance Admin Tool tab (IMPRO…
Sellafield Jun 6, 2026
e0e1dac
fix(insurance): add RETURN after RAISERROR guards in usp_RecalculateI…
Sellafield Jun 6, 2026
b8c1bb9
docs(backlog): mark IMPROVEMENT-036 DONE
Sellafield Jun 6, 2026
048bd79
fix(insurance): define BoolToVisibilityHidden locally in EconomyInsur…
Sellafield Jun 6, 2026
6b944d7
fix(insurance): increase SP timeout and make startup refresh non-bloc…
Sellafield Jun 7, 2026
ebc3e6a
Db structure
Sellafield Jun 7, 2026
0f264c9
fix(insurance): inline production_data CTE in recursive views to fix …
Sellafield Jun 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Equipment Set Season Rewards Migration (IMPROVEMENT-033)
-- Run once against the game database before deploying the updated server binary.

-- Make package_id nullable on tables where it was NOT NULL
ALTER TABLE season_tiers ALTER COLUMN package_id INT NULL;
ALTER TABLE season_leaderboard_rewards ALTER COLUMN package_id INT NULL;

-- Add equipment_set_id to all three season reward tables
ALTER TABLE season_tiers ADD equipment_set_id INT NULL REFERENCES equipment_sets(set_id);
ALTER TABLE season_objectives ADD equipment_set_id INT NULL REFERENCES equipment_sets(set_id);
ALTER TABLE season_leaderboard_rewards ADD equipment_set_id INT NULL REFERENCES equipment_sets(set_id);
298 changes: 297 additions & 1 deletion docs/backlog/improvements.md

Large diffs are not rendered by default.

177 changes: 176 additions & 1 deletion docs/backlog/issues.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,181 @@
# Last ID used

024
029

## ISSUE-029 - Insurance price recalculation crashes with SP nesting level exceeded (limit 32)

Status: DONE
Priority: CRITICAL
Area: Economy / Insurance

### Problem
On production, calling `usp_RecalculateInsurancePrices` throws:

> Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32)

The recalculation fails entirely; insurance prices are not updated.

### Root Cause (Confirmed)
Both `v_all_production_costs` and `v_required_raw_materials` contain recursive CTEs whose recursive
member JOINs against `production_data`, which is a VIEW (not a base table). SQL Server increments
the view nesting counter on every recursive iteration that references an external view. On production
data with crafting chains deeper than ~28 items the counter exceeds the 32-level limit. Locally,
sparse data means chains rarely exceed 3–5 levels, so the bug never triggers.

`usp_RecalculateInsurancePrices` executes `v_all_production_costs` inline inside a MERGE statement,
which exposes the per-iteration view nesting accumulation. `usp_RefreshAutoMarketOrders` is
unaffected because it materializes the same views into temp tables via a standalone SELECT, where
the optimizer handles the recursive CTE differently.

### Fix
Inlined `production_data` as a local CTE (`prod_data`) at the top of both recursive views.
A CTE reference inside a recursive member does not increment the view nesting counter.
Semantics are identical (same filter, same columns).

### Files Changed
- `docs/db_structure/views/v_all_production_costs.sql`
- `docs/db_structure/views/v_required_raw_materials.sql`
- `docs/db_structure/migrations/ISSUE-029-fix-view-nesting-in-recursive-cost-views.sql`

### Notes
- Migration can be applied while the server is running (`CREATE OR ALTER VIEW` is non-blocking).
- After applying, uncomment and run `EXEC dbo.usp_RecalculateInsurancePrices` to verify.

---

## ISSUE-028 - AdminTool AutoMarket: buyback orders not removed after deleting item from trade list

Status: DONE
Priority: CRITICAL
Area: AdminTool / AutoMarket

### Problem
After deleting an item from the AutoMarket trade list and running "Refresh Now", sell orders for that item were removed correctly but buy (buyback) orders remained on the market.

### Root Cause
Step 0 of `usp_RefreshAutoMarketOrders` snapshots "unbought resources" using `NOT EXISTS (SELECT 1 FROM market_orders_configuration)` to skip production-item buyback orders. When an item is deleted from `market_orders_configuration` before the SP runs, this check passes for its buyback order — the order is captured into `automarket_unbought_resources` as if it were an unfulfilled raw-material buy order. Step 1 deletes all auto orders, but Step 4 then re-inserts a new buy order for the deleted item from the `Unbought` carry-over, because the item still has a production cost in `v_all_production_costs`.

### Fix
In Step 0's `automarket_unbought_resources` insert, replaced:
```sql
AND NOT EXISTS (SELECT 1 FROM market_orders_configuration moc WHERE moc.definitionname = ed.definitionname)
```
with:
```sql
AND NOT EXISTS (SELECT 1 FROM production_data pd_check WHERE pd_check.product = ed.definitionname)
```
This classifies items by whether they can be manufactured (stable) rather than whether they are currently in the trade list (breaks on deletion).

### Files Changed
- `docs/db_structure/stored_procedures/dbo.usp_RefreshAutoMarketOrders.StoredProcedure.sql`

---

## ISSUE-027 - Sell orders at matching prices do not auto-fulfill against open buy orders

Status: DONE
Priority: CRITICAL
Area: Market / Trading

### Problem
Players report that creating a sell order at a price equal to or below an existing open buy order does not result in an automatic trade. The sell order is posted as a standing order rather than immediately matching and settling against the best available buy order.

### Impact
Market trades do not settle when they should. Players placing competitive sell orders experience no fulfillment despite valid counterpart buy orders existing, breaking the fundamental market matching expectation and potentially trapping capital in open orders.

### Root Cause
The matching condition in both `MarketCreateSellOrder` and `MarketCreateBuyOrder` was:
```csharp
if (!forMyCorporation && highestBuyOrder != null)
```
This condition completely skips automatic matching whenever the player marks their order as corporation-only (`forMyCorporation = true`), even when a matching corp-only order from the same corporation exists. Players in player corporations are the primary affected group.

Additionally, `GetHighestBuyOrder` had a minor inconsistency: the SQL column reference used `@itemDefinition` (capital D) while `SetParameter` used `@itemdefinition` (lowercase d) — and similarly `submitterEID` vs `submittereid`. These are harmless with SqlClient's case-insensitive parameter matching but were corrected for consistency.

### Fix
- `MarketCreateSellOrder.HandleRequest`: Changed condition to `highestBuyOrder != null && (!forMyCorporation || highestBuyOrder.forMembersOf == forMembersOf)` — allows corp-only sells to match against corp buy orders from the same corp, while still blocking corp sells against public buy orders.
- `MarketCreateBuyOrder.HandleRequest`: Same symmetric fix for `lowestSellOrder`.
- `MarketOrderRepository.GetHighestBuyOrder`: Normalized SQL column/parameter names to lowercase for consistency with `GetLowestSellOrder`.

---

## ISSUE-026 - AdminTool AutoMarket Orders filters not working as expected

Status: TODO
Priority: MEDIUM
Area: Admin Tool / AutoMarket

### Problem
Three distinct filter bugs on the AutoMarket → Orders view in the Admin Tool:

1. **Order type filter returns no results** — selecting a buy or sell order type filter produces an empty list regardless of actual order volume. Likely a binding or query mismatch between the selected enum/value and what the server-side filter expects.
2. **Category filter excludes child categories** — filtering by a parent category only returns items assigned directly to that category; items in sub-categories are excluded. The filter needs to match the selected category and all of its descendants.
3. **No way to reset filters** — once a filter is applied, there is no reset or clear button. Users must restart or navigate away to return to the unfiltered list.

### Impact
Operators cannot meaningfully browse or audit market orders. The broken type and category filters make it impractical to find specific orders; the lack of reset compounds the friction by trapping users in a filtered state.

### Proposed Fix
1. **Order type filter** — trace the selected value from the UI dropdown through the ViewModel command to the server query. Verify the filter value is correctly mapped to the DB column type and that the query predicate is applied (not silently dropped).
2. **Category filter** — replace the direct category equality check with a recursive or closure-based lookup that resolves all descendant category IDs for the selected node and filters on the full set (e.g. via a recursive CTE or a pre-loaded category tree walk).
3. **Reset filters** — add a "Clear Filters" button (or equivalent reset action) to the Orders view that restores all filter fields to their default/unset state and reloads the full order list.

### Notes
- Investigate whether the type filter bug is a null/default value mismatch (e.g. enum default being passed as the filter even when "All" is selected, or vice versa).
- The category tree hierarchy is likely already used elsewhere in the Admin Tool or game content — reuse the existing resolution pattern rather than introducing a new one.
- Fix all three as a single unit since they share the same view; shipping a partial fix leaves the Orders filter UX still broken.

---

## ISSUE-025 - Top leaderboard participants did not receive rewards after Active Season ended

Status: IN_PROGRESS
Priority: CRITICAL
Area: Seasons / Rewards / Leaderboard

### Problem
After "Seasons, oh May!" (end_time 2026-06-01T03:00:00) concluded, top leaderboard participants received no rewards. Root cause confirmed: data configuration error.

### Root Cause (Confirmed)
All 3 `season_leaderboard_rewards` rows have `rank_min > rank_max` (swapped fields):

| rank_min | rank_max | Package | Intended |
|---|---|---|---|
| 3 | 1 | Syndicate_Season1_Leadership1 | min=1, max=3 |
| 6 | 4 | Syndicate_Season1_Leadership2 | min=4, max=6 |
| 10 | 7 | Syndicate_Season1_Leadership3 | min=7, max=10 |

Server matching (`SeasonService.cs:399`): `rank >= r.RankMin && rank <= r.RankMax` — impossible to satisfy when min > max. Rewards were never delivered.

Compounded by `MarkLeaderboardDelivered` being called unconditionally (`SeasonService.cs:403`) even when no reward matched. All participants have `leaderboard_reward_delivered = 1`, blocking any automatic re-run.

### Fix

**Operator must apply immediately (SQL):**
```sql
-- Reset delivered flag
UPDATE season_character_points
SET leaderboard_reward_delivered = 0
WHERE season_id = (SELECT id FROM seasons WHERE name = N'Seasons, oh May!');

-- Fix swapped rank ranges
UPDATE season_leaderboard_rewards SET rank_min=1, rank_max=3
WHERE season_id=(SELECT id FROM seasons WHERE name=N'Seasons, oh May!') AND rank_min=3 AND rank_max=1;
UPDATE season_leaderboard_rewards SET rank_min=4, rank_max=6
WHERE season_id=(SELECT id FROM seasons WHERE name=N'Seasons, oh May!') AND rank_min=6 AND rank_max=4;
UPDATE season_leaderboard_rewards SET rank_min=7, rank_max=10
WHERE season_id=(SELECT id FROM seasons WHERE name=N'Seasons, oh May!') AND rank_min=10 AND rank_max=7;
```

**Code changes required:**
1. New `SeasonRedeliverLeaderboardRewards` admin request handler — re-runs reward delivery for a past ended season by ID, respecting the `leaderboard_reward_delivered` flag.
2. Admin Tool validation in `SeasonDetailViewModel.QueueSaveLeaderboardReward` — guard `rank_min ≤ rank_max` before queuing the save.

### Notes
- `DeliverLeaderboardReward` writes to the redeemable items table via `InsertRedeemableItems` — no server restart needed once the command exists.
- The re-deliver command must load leaderboard reward rows directly from the DB (not the in-memory cache, which is cleared at season end).

---

## ISSUE-024 - AutoMarket pricing structurally excludes player crafters from the production economy

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- IMPROVEMENT-036: Insurance System Overhaul
-- Apply once to the live database while the server is OFFLINE, before deploying the new build.
-- Run in order: table → procedure → clear stale policies → initial price population.

-- 1. Create insurance_config table
IF OBJECT_ID('dbo.insurance_config', 'U') IS NULL
BEGIN
CREATE TABLE dbo.insurance_config (
param_name NVARCHAR(64) NOT NULL PRIMARY KEY,
param_value FLOAT NOT NULL
);
INSERT INTO dbo.insurance_config (param_name, param_value) VALUES
('fee_pct', 0.10),
('payout_pct', 0.08);
END

-- 2. Create usp_RecalculateInsurancePrices
CREATE OR ALTER PROCEDURE dbo.usp_RecalculateInsurancePrices AS
BEGIN
SET NOCOUNT ON;

DECLARE @fee_pct FLOAT = (SELECT param_value FROM dbo.insurance_config WHERE param_name = 'fee_pct');
DECLARE @payout_pct FLOAT = (SELECT param_value FROM dbo.insurance_config WHERE param_name = 'payout_pct');

IF @fee_pct IS NULL OR @payout_pct IS NULL
BEGIN
RAISERROR('insurance_config: fee_pct and payout_pct must both be set.', 16, 1);
RETURN;
END

IF @payout_pct >= @fee_pct
BEGIN
RAISERROR('insurance_config: payout_pct must be strictly less than fee_pct to keep insurance a NIC sink.', 16, 1);
RETURN;
END

MERGE dbo.insuranceprices AS t
USING (
SELECT
ed.definition,
ROUND(vpc.production_cost_nic * @fee_pct, 0) AS fee,
ROUND(vpc.production_cost_nic * @payout_pct, 0) AS payout
FROM dbo.v_all_production_costs vpc
JOIN dbo.entitydefaults ed
ON ed.definitionname = vpc.product COLLATE DATABASE_DEFAULT
WHERE ed.definition IN (SELECT definition FROM dbo.insuranceprices)
AND vpc.production_cost_nic > 0
) AS s ON t.definition = s.definition
WHEN MATCHED THEN
UPDATE SET t.fee = s.fee, t.payout = s.payout;
END

-- 3. Clear all stale insurance policies (payout values are outdated; players repurchase at new rates)
DELETE FROM dbo.insurance;

-- 4. Populate insuranceprices immediately so the server cache loads correct values on first startup
EXEC dbo.usp_RecalculateInsurancePrices;
34 changes: 34 additions & 0 deletions docs/db_structure/migrations/IMPROVEMENT-039-economy-health.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-- IMPROVEMENT-039: Economy Health Statistics
-- Apply once to the live database before deploying the matching server and Admin Tool builds.
-- Tables: run once only (will error if tables already exist — that is intentional).
-- Procedure: CREATE OR ALTER — safe to re-run.

CREATE TABLE economy_daily_snapshot (
id INT IDENTITY(1,1) PRIMARY KEY,
snapshot_date DATE NOT NULL,
total_nic BIGINT NOT NULL,
CONSTRAINT UQ_economy_daily_snapshot_date UNIQUE (snapshot_date)
);

CREATE TABLE economy_price_index_basket (
id INT IDENTITY(1,1) PRIMARY KEY,
definition INT NOT NULL,
weight DECIMAL(5,2) NOT NULL DEFAULT 1.0
);

CREATE OR ALTER PROCEDURE usp_RecordEconomySnapshot AS
BEGIN
DECLARE @snapshot_date DATE = CAST(GETUTCDATE() AS DATE);
DECLARE @total_nic BIGINT =
ISNULL((SELECT SUM(CAST(credit AS BIGINT)) FROM characters
WHERE active = 1 AND deletedAt IS NULL), 0)
+ ISNULL((SELECT SUM(CAST(wallet AS BIGINT)) FROM corporations
WHERE active = 1 AND defaultcorp = 0), 0);

MERGE economy_daily_snapshot AS t
USING (SELECT @snapshot_date AS snapshot_date, @total_nic AS total_nic) AS s
ON t.snapshot_date = s.snapshot_date
WHEN MATCHED THEN UPDATE SET total_nic = s.total_nic
WHEN NOT MATCHED THEN INSERT (snapshot_date, total_nic)
VALUES (s.snapshot_date, s.total_nic);
END
Loading
Loading