From 5b10e95e7be59adad57ca730b5c1708fa7230359 Mon Sep 17 00:00:00 2001 From: "Dolzhukov, Viktor" Date: Sun, 10 May 2026 12:53:37 +0500 Subject: [PATCH 01/11] Add Seasons system design spec Co-Authored-By: Claude Sonnet 4.6 (cherry picked from commit 1b2b8e4b50e99241012594f669f0d3bccd386083) --- .../specs/2026-05-10-seasons-design.md | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-10-seasons-design.md diff --git a/docs/superpowers/specs/2026-05-10-seasons-design.md b/docs/superpowers/specs/2026-05-10-seasons-design.md new file mode 100644 index 0000000..1a83135 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-seasons-design.md @@ -0,0 +1,271 @@ +# Seasons System Design + +**Date:** 2026-05-10 +**Status:** Approved +**Approach:** New Season Service + Mail Feedback (Approach B) + +--- + +## Overview + +A Seasons system lets players earn rewards over a fixed time window by performing in-game activities. Each season has: + +- A **tier reward track**: accumulate season points to cross thresholds and unlock reward packages. +- A **competitive leaderboard**: top-ranked players at season end receive exclusive bonus rewards. +- **Objectives**: milestone tasks that award bonus points on completion. +- **Mail notifications**: the primary feedback channel since client code cannot be modified. + +Reward delivery uses the existing `accountredeemableitems` / `packages` infrastructure. All season configuration is database-driven; admin commands are issued via the in-game secured chat channel. + +--- + +## Constraints + +- No client code changes — all player-facing surfaces must use existing client UI. +- Reward delivery via existing `accountredeemableitems` → `RedeemableItemList` / `RedeemableItemRedeem` client flow. +- Player feedback via in-game mail (`MailHandler`). +- Season configuration via database tables; admin commands via secured in-game chat channel. + +--- + +## Data Model + +### `seasons` + +| Column | Type | Notes | +|---|---|---| +| `id` | int PK | Auto-increment | +| `name` | varchar(128) | Display name | +| `description` | varchar(512) | Shown in start mail | +| `start_time` | datetime | Season opens | +| `end_time` | datetime | Season closes | +| `is_active` | bit | Manual override; admin sets to 1 to go live | + +### `season_activity_rates` + +Maps activity types to base points per unit. Admins tune these to balance the season economy. + +| Column | Type | Notes | +|---|---|---| +| `id` | int PK | | +| `season_id` | int FK → seasons | | +| `activity_type` | int | See `SeasonActivityType` enum below | +| `points_per_unit` | float | Points awarded per unit of activity | +| `unit_scale` | int | Divisor applied to raw amount before multiplying (e.g. 1000 for "per 1000 NIC") | + +### `season_objectives` + +Milestone tasks within a season. Completing one awards bonus points on top of base rate. + +| Column | Type | Notes | +|---|---|---| +| `id` | int PK | | +| `season_id` | int FK → seasons | | +| `name` | varchar(128) | | +| `description` | varchar(512) | Shown in mail | +| `activity_type` | int | Must match a `SeasonActivityType` | +| `target_value` | bigint | e.g. 50 for "kill 50 NPCs" | +| `bonus_points` | int | Awarded once on completion | +| `display_order` | int | Ordering in status mails | + +### `season_tiers` + +Reward thresholds. Each tier links to an existing `packages` entry. + +| Column | Type | Notes | +|---|---|---| +| `id` | int PK | | +| `season_id` | int FK → seasons | | +| `tier_number` | int | Ordering (1 = lowest) | +| `tier_name` | varchar(64) | e.g. "Bronze", "Silver", "Gold" | +| `points_required` | int | Cumulative points needed | +| `package_id` | int FK → packages | Reward package to deliver | + +### `season_leaderboard_rewards` + +End-of-season competitive bonuses by rank range. + +| Column | Type | Notes | +|---|---|---| +| `id` | int PK | | +| `season_id` | int FK → seasons | | +| `rank_min` | int | Inclusive lower bound (1 = first place) | +| `rank_max` | int | Inclusive upper bound | +| `package_id` | int FK → packages | | + +### `season_character_points` + +Running totals per character per season. + +| Column | Type | Notes | +|---|---|---| +| `character_id` | int PK, FK | | +| `season_id` | int PK, FK | | +| `total_points` | bigint | Atomically incremented | +| `last_updated` | datetime | | +| `intro_mail_sent` | bit | Prevents duplicate login intro mail | +| `leaderboard_reward_delivered` | bit | Guards against double-delivery at season end | + +### `season_objective_progress` + +Per-character per-objective tracking. + +| Column | Type | Notes | +|---|---|---| +| `character_id` | int PK, FK | | +| `season_id` | int PK, FK | | +| `objective_id` | int PK, FK | | +| `current_value` | bigint | Raw activity units accumulated | +| `completed` | bit | Set when `current_value >= target_value` | +| `completed_time` | datetime | Nullable | +| `bonus_awarded` | bit | Prevents double bonus on restart | + +### `season_tier_claims` + +Tracks delivered tier rewards to prevent double-delivery. + +| Column | Type | Notes | +|---|---|---| +| `character_id` | int PK, FK | | +| `season_id` | int PK, FK | | +| `tier_id` | int PK, FK | | +| `claimed_time` | datetime | | + +--- + +## SeasonActivityType Enum + +```csharp +public enum SeasonActivityType +{ + NpcKill = 1, + PvpKill = 2, + MissionComplete = 3, + MineralMined = 4, // units harvested/drilled + EpSpent = 5, + NicEarned = 6, + NicSpent = 7, + IntrusionPoint = 8, +} +``` + +--- + +## Service Architecture + +### `SeasonService` + +Singleton registered via Autofac in a new `SeasonModule`. Responsibilities: + +- Loads and caches the active season (rates, objectives, tiers) from DB on startup and every 5 minutes. +- Exposes `RecordActivity(int characterId, SeasonActivityType type, long amount)` for all event hooks to call. +- Runs a background timer for end-of-season detection and processing. + +**`RecordActivity` flow:** + +1. If no active season or season has ended, return immediately. +2. Compute base points: `floor(amount / unit_scale) * points_per_unit`. +3. Atomically increment `season_character_points.total_points` (upsert). +4. For each objective matching `activity_type`: increment `current_value`. +5. For any objective newly completed: award bonus points, set `bonus_awarded`, send objective-complete mail. +6. For any tier threshold newly crossed (not yet claimed): insert `season_tier_claims`, insert into `accountredeemableitems`, send tier-unlock mail. + +**End-of-season timer:** + +Fires every minute, checks if `end_time` has passed and `is_active = 1`: + +1. Set `is_active = 0`. +2. Rank all characters in `season_character_points` by `total_points` DESC. +3. For each character, match rank to `season_leaderboard_rewards` ranges. +4. Deliver matching reward packages via `accountredeemableitems` (guarded by `leaderboard_reward_delivered` flag). +5. Send final-standings mail to every participant. + +### Event Hooks + +`SeasonService` subscribes to existing game events at startup: + +| Activity | Hook point | +|---|---| +| NPC kill | `SmartCreature` death event, extract killer character | +| PvP kill | Player death event in zone | +| Mission complete | `MissionInProgress` completion callback | +| Mining / harvesting | Production/gathering completion event | +| EP spent | `EpForActivityLogEvent` when EP is consumed on extension | +| NIC earned | `CharacterWallet` credit transaction log (credit events) | +| NIC spent | `CharacterWallet` credit transaction log (debit events) | +| Intrusion point | SAP/intrusion completion event in zone | + +--- + +## Reward Delivery + +Tier and leaderboard rewards are both delivered via `accountredeemableitems`: + +1. Look up items in `packageitems` for the target `package_id`. +2. Insert one row per item into `accountredeemableitems` (`wasredeemed = 0`). +3. Player redeems via existing `RedeemableItemList` / `RedeemableItemRedeem` client commands at any terminal. + +**Double-delivery guards:** + +- Tiers: insert `season_tier_claims` row first; skip if already present. +- Leaderboard: skip characters where `leaderboard_reward_delivered = 1`. + +--- + +## Mail Notifications + +All mails sent via existing `MailHandler`. + +| Trigger | Recipients | Content | +|---|---|---| +| Season activates | All online characters | Season name, duration, objective list, tier thresholds | +| Character logs in during active season (first time) | That character | Same as activation mail; gated by `intro_mail_sent` flag | +| Objective completed | That character | Objective name, bonus points, running total | +| Tier unlocked | That character | Tier name, points reached, redeem reminder | +| Season ends | All participants | Final rank, total points, leaderboard reward notification if applicable | + +--- + +## In-Game Admin Commands + +All commands use the existing `[ChatCommand]` attribute pattern in `AdminCommandHandlers.cs` (or a new `SeasonAdminCommandHandlers.cs`). They are automatically restricted to: + +1. **Secured channel** — admin must issue `#Secure` first; commands are blocked in unsecured channels. +2. **`AccessLevel.admin`** — enforced by `Session.cs` before the handler is invoked. + +| Command | Arguments | Effect | +|---|---|---| +| `#SeasonCreate` | ` ` | Inserts into `seasons` (`is_active=0`). **Replies with generated `seasonId`.** | +| `#SeasonActivate` | `` | Sets `is_active=1`, triggers `SeasonService` cache refresh | +| `#SeasonDeactivate` | `` | Sets `is_active=0` | +| `#SeasonAddRate` | ` ` | Inserts/updates `season_activity_rates` | +| `#SeasonAddObjective` | ` ` | Inserts into `season_objectives` | +| `#SeasonAddTier` | ` ` | Inserts into `season_tiers` | +| `#SeasonAddLeaderboard` | ` ` | Inserts into `season_leaderboard_rewards` | +| `#SeasonStatus` | _(none)_ | Prints active season name, time remaining, participant count | +| `#SeasonInfo` | `` | Prints full config: rates, objectives, tiers, leaderboard rewards | +| `#SeasonForceEnd` | `` | Immediately triggers end-of-season processing | + +--- + +## Project Layout (New Files) + +Following existing patterns in the codebase: + +| File | Role | +|---|---| +| `src/Perpetuum/Services/Seasons/SeasonService.cs` | Core service: event hooks, point recording, tier/leaderboard delivery | +| `src/Perpetuum/Services/Seasons/SeasonRepository.cs` | DB access for all season tables | +| `src/Perpetuum/Services/Seasons/SeasonModels.cs` | Domain models: `Season`, `SeasonObjective`, `SeasonTier`, etc. | +| `src/Perpetuum/Services/Seasons/SeasonActivityType.cs` | `SeasonActivityType` enum | +| `src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs` | Autofac registration | +| `src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs` | `[ChatCommand]` handlers for admin commands | + +--- + +## Out of Scope (This Phase) + +- Admin web UI for season management +- Client-side season panel or progress display +- Per-zone or per-faction season variants +- Season pass / premium tier track From 3bde15be2c0fa4f007d2335393961c348ace3389 Mon Sep 17 00:00:00 2001 From: "Dolzhukov, Viktor" Date: Sun, 10 May 2026 16:46:49 +0500 Subject: [PATCH 02/11] Claude setup --- .claude/settings.json | 5 + .gitignore | 1 + CLAUDE.md | 227 + docs/ROBOT_TEMPLATE_SAMPLE.md | 22 + docs/db_structure/data_types/IntList.sql | 5 + .../data_types/integer_list_tbltype.sql | 9 + .../data_types/long_list_tbltype.sql | 9 + .../database_schema_documentation.md | 6983 +++++++++++++++++ docs/db_structure/functions/CFName.sql | 24 + .../functions/DynpropRemoveKey.sql | 46 + docs/db_structure/functions/GetCEO.sql | 28 + docs/db_structure/functions/GetCFMask.sql | 30 + .../functions/GetCorporationName.sql | 26 + .../functions/GetDefinitionName.sql | 27 + docs/db_structure/functions/GetNick.sql | 27 + .../GetPublicContainerEidByBaseEid.sql | 23 + docs/db_structure/functions/GetRandomEid.sql | 31 + docs/db_structure/functions/GetTableStats.sql | 23 + .../functions/GetTransactionTypeName.sql | 24 + docs/db_structure/functions/GuidToUid.sql | 24 + .../functions/IsCampaignEligible.sql | 32 + .../functions/IsDefinitionRepackable.sql | 34 + docs/db_structure/functions/ToHex.sql | 29 + .../functions/TryGetRandomEid.sql | 27 + .../functions/accountPackageHasItem.sql | 66 + .../functions/accountPackageHasItemTest.sql | 66 + .../functions/accountPackageIsPurchased.sql | 30 + .../functions/activityNameByType.sql | 67 + .../functions/aggregateFieldsUsed.sql | 31 + .../functions/aggregateInfoByCFString.sql | 76 + .../functions/aggregateInfoByDefList.sql | 78 + .../functions/aggregateInfoByIds.sql | 71 + .../functions/aggregateRecordsByCfString.sql | 37 + .../functions/aggregateRecordsByDefList.sql | 38 + .../functions/aggregateRecordsByIds.sql | 42 + .../functions/aggregateValueSeries.sql | 69 + .../functions/allCodingObjects.sql | 23 + .../functions/allPossibleOwners.sql | 21 + .../functions/bestWorstValues.sql | 58 + docs/db_structure/functions/blend.sql | 26 + docs/db_structure/functions/calcPrice.sql | 79 + docs/db_structure/functions/childrenCount.sql | 27 + .../db_structure/functions/counticeingame.sql | 21 + .../functions/countpurchasedice.sql | 20 + .../functions/countredeemedice.sql | 21 + .../functions/creditOrEpByDefinition.sql | 64 + docs/db_structure/functions/defNameSeries.sql | 70 + docs/db_structure/functions/distance2d.sql | 28 + .../functions/eidToInsertString.sql | 41 + .../functions/emailByAccountId.sql | 21 + .../db_structure/functions/ewModulesInHex.sql | 30 + .../functions/extensionPointsAvailable.sql | 31 + .../functions/extensionPointsCollected.sql | 29 + .../extensionSubscriptionIsAcitve.sql | 25 + .../functions/extremeDefsByCfField.sql | 39 + .../fn_CalculateDynamicPlasmaPrices.sql | 80 + .../functions/getAggregateName.sql | 24 + .../db_structure/functions/getAllianceEid.sql | 24 + .../functions/getBaseEIDByCharacterID.sql | 27 + .../getBetaArtifactIdFromAlphaId.sql | 42 + docs/db_structure/functions/getCalendar.sql | 62 + .../functions/getCharacterEID.sql | 28 + .../functions/getCorporationEid.sql | 22 + .../functions/getCorporationEidAndRole.sql | 19 + .../getCorporationNameByCharacterEID.sql | 30 + .../getCorporationNameByCharacterID.sql | 29 + .../getDefaultDockingBasePositions.sql | 31 + .../functions/getDefinitionByCF.sql | 24 + .../functions/getDefinitionByCFString.sql | 27 + .../functions/getDefinitionByCategoryflag.sql | 24 + .../functions/getDefinitionByEID.sql | 22 + .../getDockingbaseChildrenFromActiveZones.sql | 30 + .../functions/getDockingbaseDefinitions.sql | 19 + .../functions/getEntitiesFromZoneByCf.sql | 30 + .../functions/getIndexFragmentationData.sql | 34 + .../functions/getInnerBetaZoneIdFromAlpha.sql | 31 + docs/db_structure/functions/getIpStatus.sql | 53 + .../functions/getLiveBetaMarkets.sql | 26 + .../functions/getLiveDefaultMarkets.sql | 24 + .../functions/getLiveDockingbaseChildren.sql | 28 + .../getLiveFieldTerminalChildren.sql | 28 + .../functions/getLiveGammaDockingBases.sql | 21 + .../functions/getLiveGammaMarkets.sql | 20 + .../functions/getLiveStructureChildren.sql | 28 + .../functions/getMissionCategoryName.sql | 22 + docs/db_structure/functions/getNickByEid.sql | 22 + .../functions/getPBSDefinitionFromCapsule.sql | 35 + docs/db_structure/functions/getTree.sql | 32 + docs/db_structure/functions/getTreeByRoot.sql | 32 + .../functions/getVendorMarketPrice.sql | 48 + .../functions/getVendorSellPrice.sql | 34 + docs/db_structure/functions/getdaystring.sql | 30 + .../functions/intToInsertString.sql | 0 .../functions/invalidBaseChannels.sql | 25 + .../db_structure/functions/isAccountTrial.sql | 29 + .../isAggregateFieldMoreIsBetter.sql | 34 + .../functions/isDefinitionSparkUnlocker.sql | 31 + .../db_structure/functions/isEntityExists.sql | 24 + docs/db_structure/functions/isInTree.sql | 42 + docs/db_structure/functions/isInstance.sql | 23 + .../listItemsLeftInFieldContainer.sql | 38 + .../functions/liveRandomMissions.sql | 18 + .../functions/missionLocationsFromZone.sql | 20 + .../functions/onlineGangMembers.sql | 27 + docs/db_structure/functions/padLeft.sql | 19 + .../functions/possibleNewMissions.sql | 23 + .../productionFacilitiesByPattern.sql | 38 + .../functions/productionFacilityByLevel.sql | 27 + .../productionFacilityFromBaseByPattern.sql | 23 + docs/db_structure/functions/randomHyp.sql | 30 + .../functions/robotTemplateContainsEw.sql | 60 + .../functions/sha1FromVarchar.sql | 21 + docs/db_structure/functions/showDynProp.sql | 56 + docs/db_structure/functions/splitString.sql | 47 + .../functions/stringToInsertString.sql | 32 + .../functions/teleportColumns.sql | 26 + docs/db_structure/functions/treeEids.sql | 33 + .../tutorialMissionTargetsOnZone.sql | 37 + .../functions/tutorialStructures.sql | 39 + .../CreateFolderContainer.sql | 50 + .../stored_procedures/DeleteAllGang.sql | 23 + .../DuplicateDefinitionConfig.sql | 103 + .../stored_procedures/EpForActivityByType.sql | 63 + .../stored_procedures/GetNpcKillEp.sql | 19 + .../stored_procedures/accountAddCredit.sql | 24 + .../accountAllocateSteamKey.sql | 39 + .../accountCreditEnqueue.sql | 21 + .../stored_procedures/accountEmailConfirm.sql | 20 + .../accountPackageBought.sql | 31 + .../accountPackageGenerateAll.sql | 43 + .../accountPackageGenerateSpark.sql | 58 + .../stored_procedures/accountPackageHas.sql | 20 + .../stored_procedures/accountPackageList.sql | 20 + .../accountPackageProcessOne.sql | 57 + .../stored_procedures/accountPurchase.sql | 53 + .../stored_procedures/accountSimpleDelete.sql | 30 + .../accountonlinetimestart.sql | 41 + .../accountonlinetimestop.sql | 25 + .../addCampaignGoodiePack.sql | 40 + .../addDefinitionToBetaMarkets.sql | 61 + .../addDefinitionToDefaultMarkets.sql | 61 + docs/db_structure/stored_procedures/addIP.sql | 54 + .../stored_procedures/addKill.sql | 58 + .../stored_procedures/addVendorBuyItem.sql | 82 + .../stored_procedures/addVendorSellItem.sql | 83 + .../stored_procedures/addownerincome.sql | 34 + .../addpresettorandompool.sql | 23 + .../stored_procedures/artifactReset.sql | 22 + .../stored_procedures/backupCurrentDbFull.sql | 32 + .../stored_procedures/backupLog.sql | 40 + .../stored_procedures/centralBank_add.sql | 38 + .../stored_procedures/centralBank_addLog.sql | 31 + .../stored_procedures/centralBank_sub.sql | 41 + .../stored_procedures/changeEIDinOneTable.sql | 32 + .../stored_procedures/changeFacilities.sql | 111 + .../stored_procedures/channelAddBan.sql | 22 + .../stored_procedures/channelMemberAdd.sql | 25 + .../stored_procedures/characterDockToTMA.sql | 31 + .../characterSettingsSetString.sql | 32 + .../checkGameConsistency.sql | 51 + .../checkSyntaxInAllCode.sql | 161 + .../cleanDisabledDefinitions.sql | 22 + .../stored_procedures/cleanUpGame.sql | 67 + .../cleanUpInsuranceByBaseEid.sql | 23 + .../stored_procedures/cleanUpMarket.sql | 19 + .../cleanUpZoneUserEntities.sql | 19 + .../consolidate_statistics.sql | 73 + .../stored_procedures/copyArtifactLoot.sql | 31 + .../stored_procedures/copyComponents.sql | 38 + .../copyLVL1ArtifactLoot.sql | 33 + .../copyLVL3ArtifactLoot.sql | 35 + .../stored_procedures/copycharacterwizard.sql | 129 + .../stored_procedures/countParents.sql | 36 + .../stored_procedures/createVendor.sql | 58 + .../stored_procedures/debug_crm.sql | 116 + .../stored_procedures/debug_devserver.sql | 54 + .../stored_procedures/debug_disablenpcs.sql | 20 + .../stored_procedures/debug_disablezones.sql | 16 + .../debug_enableallzones.sql | 31 + .../stored_procedures/debug_enablenpcs.sql | 21 + .../stored_procedures/debug_junior.sql | 34 + .../stored_procedures/debug_liveserver.sql | 59 + .../stored_procedures/deleteAllChildren.sql | 33 + .../stored_procedures/deleteTree.sql | 41 + .../deleteUnusedPublicChannels.sql | 51 + .../dockInCharacterAndRobot.sql | 49 + .../entitiesFixParenting.sql | 32 + .../entitiesReportAndDeleteOrphanedByCf.sql | 40 + .../epForActivityLogList.sql | 28 + .../stored_procedures/extensionPointsAdd.sql | 37 + .../extensionPointsCheck.sql | 30 + .../extensionPointsConsolidate.sql | 41 + .../extensionPointsInject.sql | 22 + .../stored_procedures/extensionRevert.sql | 44 + .../stored_procedures/extensionRevertV2.sql | 68 + .../extensionSubscriptionStart.sql | 22 + .../extensionsRevertFromDate.sql | 80 + .../fieldTerminal_itemCount.sql | 26 + .../stored_procedures/findContainerRoot.sql | 42 + .../stored_procedures/freshNewsCount.sql | 37 + .../stored_procedures/getAllChildrenEids.sql | 34 + .../stored_procedures/getEntityInfo.sql | 31 + .../stored_procedures/getExtensionBonus.sql | 24 + .../stored_procedures/getFullRobot.sql | 55 + .../stored_procedures/getFullTree.sql | 47 + .../stored_procedures/getFullTreeByRoot.sql | 37 + .../stored_procedures/getItemSummary.sql | 39 + .../stored_procedures/getList.sql | 67 + .../stored_procedures/getList2.sql | 57 + .../getMissionAverageTime.sql | 39 + .../stored_procedures/getModulesFromRobot.sql | 30 + .../stored_procedures/getStructureRoot.sql | 38 + .../stored_procedures/getTableColumnInfo.sql | 20 + .../stored_procedures/getTreeEIDs.sql | 34 + .../stored_procedures/getTreeNonFiltered.sql | 40 + .../getcorporationstrength.sql | 26 + .../increaseExtensionLevel.sql | 34 + .../stored_procedures/indexesMaintenance.sql | 67 + .../stored_procedures/initServer.sql | 61 + .../stored_procedures/initchannels.sql | 55 + .../stored_procedures/insertAveragePrice.sql | 57 + .../stored_procedures/insertMarketItem.sql | 0 docs/db_structure/views/foreignKeys.sql | 26 + docs/db_structure/views/production_data.sql | 21 + docs/db_structure/views/randomView.sql | 11 + .../views/v_all_production_costs.sql | 82 + .../views/v_required_raw_materials.sql | 44 + .../views/view_itemresearchlevels.sql | 125 + docs/db_structure/views/view_prototypes.sql | 125 + 229 files changed, 15913 insertions(+) create mode 100644 .claude/settings.json create mode 100644 CLAUDE.md create mode 100644 docs/ROBOT_TEMPLATE_SAMPLE.md create mode 100644 docs/db_structure/data_types/IntList.sql create mode 100644 docs/db_structure/data_types/integer_list_tbltype.sql create mode 100644 docs/db_structure/data_types/long_list_tbltype.sql create mode 100644 docs/db_structure/database_schema_documentation.md create mode 100644 docs/db_structure/functions/CFName.sql create mode 100644 docs/db_structure/functions/DynpropRemoveKey.sql create mode 100644 docs/db_structure/functions/GetCEO.sql create mode 100644 docs/db_structure/functions/GetCFMask.sql create mode 100644 docs/db_structure/functions/GetCorporationName.sql create mode 100644 docs/db_structure/functions/GetDefinitionName.sql create mode 100644 docs/db_structure/functions/GetNick.sql create mode 100644 docs/db_structure/functions/GetPublicContainerEidByBaseEid.sql create mode 100644 docs/db_structure/functions/GetRandomEid.sql create mode 100644 docs/db_structure/functions/GetTableStats.sql create mode 100644 docs/db_structure/functions/GetTransactionTypeName.sql create mode 100644 docs/db_structure/functions/GuidToUid.sql create mode 100644 docs/db_structure/functions/IsCampaignEligible.sql create mode 100644 docs/db_structure/functions/IsDefinitionRepackable.sql create mode 100644 docs/db_structure/functions/ToHex.sql create mode 100644 docs/db_structure/functions/TryGetRandomEid.sql create mode 100644 docs/db_structure/functions/accountPackageHasItem.sql create mode 100644 docs/db_structure/functions/accountPackageHasItemTest.sql create mode 100644 docs/db_structure/functions/accountPackageIsPurchased.sql create mode 100644 docs/db_structure/functions/activityNameByType.sql create mode 100644 docs/db_structure/functions/aggregateFieldsUsed.sql create mode 100644 docs/db_structure/functions/aggregateInfoByCFString.sql create mode 100644 docs/db_structure/functions/aggregateInfoByDefList.sql create mode 100644 docs/db_structure/functions/aggregateInfoByIds.sql create mode 100644 docs/db_structure/functions/aggregateRecordsByCfString.sql create mode 100644 docs/db_structure/functions/aggregateRecordsByDefList.sql create mode 100644 docs/db_structure/functions/aggregateRecordsByIds.sql create mode 100644 docs/db_structure/functions/aggregateValueSeries.sql create mode 100644 docs/db_structure/functions/allCodingObjects.sql create mode 100644 docs/db_structure/functions/allPossibleOwners.sql create mode 100644 docs/db_structure/functions/bestWorstValues.sql create mode 100644 docs/db_structure/functions/blend.sql create mode 100644 docs/db_structure/functions/calcPrice.sql create mode 100644 docs/db_structure/functions/childrenCount.sql create mode 100644 docs/db_structure/functions/counticeingame.sql create mode 100644 docs/db_structure/functions/countpurchasedice.sql create mode 100644 docs/db_structure/functions/countredeemedice.sql create mode 100644 docs/db_structure/functions/creditOrEpByDefinition.sql create mode 100644 docs/db_structure/functions/defNameSeries.sql create mode 100644 docs/db_structure/functions/distance2d.sql create mode 100644 docs/db_structure/functions/eidToInsertString.sql create mode 100644 docs/db_structure/functions/emailByAccountId.sql create mode 100644 docs/db_structure/functions/ewModulesInHex.sql create mode 100644 docs/db_structure/functions/extensionPointsAvailable.sql create mode 100644 docs/db_structure/functions/extensionPointsCollected.sql create mode 100644 docs/db_structure/functions/extensionSubscriptionIsAcitve.sql create mode 100644 docs/db_structure/functions/extremeDefsByCfField.sql create mode 100644 docs/db_structure/functions/fn_CalculateDynamicPlasmaPrices.sql create mode 100644 docs/db_structure/functions/getAggregateName.sql create mode 100644 docs/db_structure/functions/getAllianceEid.sql create mode 100644 docs/db_structure/functions/getBaseEIDByCharacterID.sql create mode 100644 docs/db_structure/functions/getBetaArtifactIdFromAlphaId.sql create mode 100644 docs/db_structure/functions/getCalendar.sql create mode 100644 docs/db_structure/functions/getCharacterEID.sql create mode 100644 docs/db_structure/functions/getCorporationEid.sql create mode 100644 docs/db_structure/functions/getCorporationEidAndRole.sql create mode 100644 docs/db_structure/functions/getCorporationNameByCharacterEID.sql create mode 100644 docs/db_structure/functions/getCorporationNameByCharacterID.sql create mode 100644 docs/db_structure/functions/getDefaultDockingBasePositions.sql create mode 100644 docs/db_structure/functions/getDefinitionByCF.sql create mode 100644 docs/db_structure/functions/getDefinitionByCFString.sql create mode 100644 docs/db_structure/functions/getDefinitionByCategoryflag.sql create mode 100644 docs/db_structure/functions/getDefinitionByEID.sql create mode 100644 docs/db_structure/functions/getDockingbaseChildrenFromActiveZones.sql create mode 100644 docs/db_structure/functions/getDockingbaseDefinitions.sql create mode 100644 docs/db_structure/functions/getEntitiesFromZoneByCf.sql create mode 100644 docs/db_structure/functions/getIndexFragmentationData.sql create mode 100644 docs/db_structure/functions/getInnerBetaZoneIdFromAlpha.sql create mode 100644 docs/db_structure/functions/getIpStatus.sql create mode 100644 docs/db_structure/functions/getLiveBetaMarkets.sql create mode 100644 docs/db_structure/functions/getLiveDefaultMarkets.sql create mode 100644 docs/db_structure/functions/getLiveDockingbaseChildren.sql create mode 100644 docs/db_structure/functions/getLiveFieldTerminalChildren.sql create mode 100644 docs/db_structure/functions/getLiveGammaDockingBases.sql create mode 100644 docs/db_structure/functions/getLiveGammaMarkets.sql create mode 100644 docs/db_structure/functions/getLiveStructureChildren.sql create mode 100644 docs/db_structure/functions/getMissionCategoryName.sql create mode 100644 docs/db_structure/functions/getNickByEid.sql create mode 100644 docs/db_structure/functions/getPBSDefinitionFromCapsule.sql create mode 100644 docs/db_structure/functions/getTree.sql create mode 100644 docs/db_structure/functions/getTreeByRoot.sql create mode 100644 docs/db_structure/functions/getVendorMarketPrice.sql create mode 100644 docs/db_structure/functions/getVendorSellPrice.sql create mode 100644 docs/db_structure/functions/getdaystring.sql create mode 100644 docs/db_structure/functions/intToInsertString.sql create mode 100644 docs/db_structure/functions/invalidBaseChannels.sql create mode 100644 docs/db_structure/functions/isAccountTrial.sql create mode 100644 docs/db_structure/functions/isAggregateFieldMoreIsBetter.sql create mode 100644 docs/db_structure/functions/isDefinitionSparkUnlocker.sql create mode 100644 docs/db_structure/functions/isEntityExists.sql create mode 100644 docs/db_structure/functions/isInTree.sql create mode 100644 docs/db_structure/functions/isInstance.sql create mode 100644 docs/db_structure/functions/listItemsLeftInFieldContainer.sql create mode 100644 docs/db_structure/functions/liveRandomMissions.sql create mode 100644 docs/db_structure/functions/missionLocationsFromZone.sql create mode 100644 docs/db_structure/functions/onlineGangMembers.sql create mode 100644 docs/db_structure/functions/padLeft.sql create mode 100644 docs/db_structure/functions/possibleNewMissions.sql create mode 100644 docs/db_structure/functions/productionFacilitiesByPattern.sql create mode 100644 docs/db_structure/functions/productionFacilityByLevel.sql create mode 100644 docs/db_structure/functions/productionFacilityFromBaseByPattern.sql create mode 100644 docs/db_structure/functions/randomHyp.sql create mode 100644 docs/db_structure/functions/robotTemplateContainsEw.sql create mode 100644 docs/db_structure/functions/sha1FromVarchar.sql create mode 100644 docs/db_structure/functions/showDynProp.sql create mode 100644 docs/db_structure/functions/splitString.sql create mode 100644 docs/db_structure/functions/stringToInsertString.sql create mode 100644 docs/db_structure/functions/teleportColumns.sql create mode 100644 docs/db_structure/functions/treeEids.sql create mode 100644 docs/db_structure/functions/tutorialMissionTargetsOnZone.sql create mode 100644 docs/db_structure/functions/tutorialStructures.sql create mode 100644 docs/db_structure/stored_procedures/CreateFolderContainer.sql create mode 100644 docs/db_structure/stored_procedures/DeleteAllGang.sql create mode 100644 docs/db_structure/stored_procedures/DuplicateDefinitionConfig.sql create mode 100644 docs/db_structure/stored_procedures/EpForActivityByType.sql create mode 100644 docs/db_structure/stored_procedures/GetNpcKillEp.sql create mode 100644 docs/db_structure/stored_procedures/accountAddCredit.sql create mode 100644 docs/db_structure/stored_procedures/accountAllocateSteamKey.sql create mode 100644 docs/db_structure/stored_procedures/accountCreditEnqueue.sql create mode 100644 docs/db_structure/stored_procedures/accountEmailConfirm.sql create mode 100644 docs/db_structure/stored_procedures/accountPackageBought.sql create mode 100644 docs/db_structure/stored_procedures/accountPackageGenerateAll.sql create mode 100644 docs/db_structure/stored_procedures/accountPackageGenerateSpark.sql create mode 100644 docs/db_structure/stored_procedures/accountPackageHas.sql create mode 100644 docs/db_structure/stored_procedures/accountPackageList.sql create mode 100644 docs/db_structure/stored_procedures/accountPackageProcessOne.sql create mode 100644 docs/db_structure/stored_procedures/accountPurchase.sql create mode 100644 docs/db_structure/stored_procedures/accountSimpleDelete.sql create mode 100644 docs/db_structure/stored_procedures/accountonlinetimestart.sql create mode 100644 docs/db_structure/stored_procedures/accountonlinetimestop.sql create mode 100644 docs/db_structure/stored_procedures/addCampaignGoodiePack.sql create mode 100644 docs/db_structure/stored_procedures/addDefinitionToBetaMarkets.sql create mode 100644 docs/db_structure/stored_procedures/addDefinitionToDefaultMarkets.sql create mode 100644 docs/db_structure/stored_procedures/addIP.sql create mode 100644 docs/db_structure/stored_procedures/addKill.sql create mode 100644 docs/db_structure/stored_procedures/addVendorBuyItem.sql create mode 100644 docs/db_structure/stored_procedures/addVendorSellItem.sql create mode 100644 docs/db_structure/stored_procedures/addownerincome.sql create mode 100644 docs/db_structure/stored_procedures/addpresettorandompool.sql create mode 100644 docs/db_structure/stored_procedures/artifactReset.sql create mode 100644 docs/db_structure/stored_procedures/backupCurrentDbFull.sql create mode 100644 docs/db_structure/stored_procedures/backupLog.sql create mode 100644 docs/db_structure/stored_procedures/centralBank_add.sql create mode 100644 docs/db_structure/stored_procedures/centralBank_addLog.sql create mode 100644 docs/db_structure/stored_procedures/centralBank_sub.sql create mode 100644 docs/db_structure/stored_procedures/changeEIDinOneTable.sql create mode 100644 docs/db_structure/stored_procedures/changeFacilities.sql create mode 100644 docs/db_structure/stored_procedures/channelAddBan.sql create mode 100644 docs/db_structure/stored_procedures/channelMemberAdd.sql create mode 100644 docs/db_structure/stored_procedures/characterDockToTMA.sql create mode 100644 docs/db_structure/stored_procedures/characterSettingsSetString.sql create mode 100644 docs/db_structure/stored_procedures/checkGameConsistency.sql create mode 100644 docs/db_structure/stored_procedures/checkSyntaxInAllCode.sql create mode 100644 docs/db_structure/stored_procedures/cleanDisabledDefinitions.sql create mode 100644 docs/db_structure/stored_procedures/cleanUpGame.sql create mode 100644 docs/db_structure/stored_procedures/cleanUpInsuranceByBaseEid.sql create mode 100644 docs/db_structure/stored_procedures/cleanUpMarket.sql create mode 100644 docs/db_structure/stored_procedures/cleanUpZoneUserEntities.sql create mode 100644 docs/db_structure/stored_procedures/consolidate_statistics.sql create mode 100644 docs/db_structure/stored_procedures/copyArtifactLoot.sql create mode 100644 docs/db_structure/stored_procedures/copyComponents.sql create mode 100644 docs/db_structure/stored_procedures/copyLVL1ArtifactLoot.sql create mode 100644 docs/db_structure/stored_procedures/copyLVL3ArtifactLoot.sql create mode 100644 docs/db_structure/stored_procedures/copycharacterwizard.sql create mode 100644 docs/db_structure/stored_procedures/countParents.sql create mode 100644 docs/db_structure/stored_procedures/createVendor.sql create mode 100644 docs/db_structure/stored_procedures/debug_crm.sql create mode 100644 docs/db_structure/stored_procedures/debug_devserver.sql create mode 100644 docs/db_structure/stored_procedures/debug_disablenpcs.sql create mode 100644 docs/db_structure/stored_procedures/debug_disablezones.sql create mode 100644 docs/db_structure/stored_procedures/debug_enableallzones.sql create mode 100644 docs/db_structure/stored_procedures/debug_enablenpcs.sql create mode 100644 docs/db_structure/stored_procedures/debug_junior.sql create mode 100644 docs/db_structure/stored_procedures/debug_liveserver.sql create mode 100644 docs/db_structure/stored_procedures/deleteAllChildren.sql create mode 100644 docs/db_structure/stored_procedures/deleteTree.sql create mode 100644 docs/db_structure/stored_procedures/deleteUnusedPublicChannels.sql create mode 100644 docs/db_structure/stored_procedures/dockInCharacterAndRobot.sql create mode 100644 docs/db_structure/stored_procedures/entitiesFixParenting.sql create mode 100644 docs/db_structure/stored_procedures/entitiesReportAndDeleteOrphanedByCf.sql create mode 100644 docs/db_structure/stored_procedures/epForActivityLogList.sql create mode 100644 docs/db_structure/stored_procedures/extensionPointsAdd.sql create mode 100644 docs/db_structure/stored_procedures/extensionPointsCheck.sql create mode 100644 docs/db_structure/stored_procedures/extensionPointsConsolidate.sql create mode 100644 docs/db_structure/stored_procedures/extensionPointsInject.sql create mode 100644 docs/db_structure/stored_procedures/extensionRevert.sql create mode 100644 docs/db_structure/stored_procedures/extensionRevertV2.sql create mode 100644 docs/db_structure/stored_procedures/extensionSubscriptionStart.sql create mode 100644 docs/db_structure/stored_procedures/extensionsRevertFromDate.sql create mode 100644 docs/db_structure/stored_procedures/fieldTerminal_itemCount.sql create mode 100644 docs/db_structure/stored_procedures/findContainerRoot.sql create mode 100644 docs/db_structure/stored_procedures/freshNewsCount.sql create mode 100644 docs/db_structure/stored_procedures/getAllChildrenEids.sql create mode 100644 docs/db_structure/stored_procedures/getEntityInfo.sql create mode 100644 docs/db_structure/stored_procedures/getExtensionBonus.sql create mode 100644 docs/db_structure/stored_procedures/getFullRobot.sql create mode 100644 docs/db_structure/stored_procedures/getFullTree.sql create mode 100644 docs/db_structure/stored_procedures/getFullTreeByRoot.sql create mode 100644 docs/db_structure/stored_procedures/getItemSummary.sql create mode 100644 docs/db_structure/stored_procedures/getList.sql create mode 100644 docs/db_structure/stored_procedures/getList2.sql create mode 100644 docs/db_structure/stored_procedures/getMissionAverageTime.sql create mode 100644 docs/db_structure/stored_procedures/getModulesFromRobot.sql create mode 100644 docs/db_structure/stored_procedures/getStructureRoot.sql create mode 100644 docs/db_structure/stored_procedures/getTableColumnInfo.sql create mode 100644 docs/db_structure/stored_procedures/getTreeEIDs.sql create mode 100644 docs/db_structure/stored_procedures/getTreeNonFiltered.sql create mode 100644 docs/db_structure/stored_procedures/getcorporationstrength.sql create mode 100644 docs/db_structure/stored_procedures/increaseExtensionLevel.sql create mode 100644 docs/db_structure/stored_procedures/indexesMaintenance.sql create mode 100644 docs/db_structure/stored_procedures/initServer.sql create mode 100644 docs/db_structure/stored_procedures/initchannels.sql create mode 100644 docs/db_structure/stored_procedures/insertAveragePrice.sql create mode 100644 docs/db_structure/stored_procedures/insertMarketItem.sql create mode 100644 docs/db_structure/views/foreignKeys.sql create mode 100644 docs/db_structure/views/production_data.sql create mode 100644 docs/db_structure/views/randomView.sql create mode 100644 docs/db_structure/views/v_all_production_costs.sql create mode 100644 docs/db_structure/views/v_required_raw_materials.sql create mode 100644 docs/db_structure/views/view_itemresearchlevels.sql create mode 100644 docs/db_structure/views/view_prototypes.sql 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..6afbe7b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ src/Perpetuum.ServerService2/data/layers/ src/Perpetuum.ServerService2/data/logs/ bin/ Releases/ +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a39a695 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,227 @@ +# 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. It manages player sessions, zones (game world instances), combat, robot equipment modules, NPCs, missions, market trading, crafting, and player-built structures. Built on .NET 8, targeting x64 Windows only. + +## Change Guidelines + +- Don't assume. Don't hide confusion. Surface tradeoffs. +- Minimum code that solves the problem. Nothing speculative. +- Touch only what you must. Clean up only your own mess. +- Define success criteria. Loop until verified. +- Prefer minimal, focused changes - do not refactor unrelated areas. +- Mirror existing folder and file patterns when adding features. +- Keep naming consistent with surrounding code. +- Update [`.claude/knowledge/architecture.md`](.claude/knowledge/architecture.md) when introducing major architectural changes. +- Don't make any code commits unless explicitly asked. + +## Build & Run + +```bash +# Build (Release x64) +dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64 + +# Run as console app (requires a configured GameRoot with perpetuum.ini and database) +cd src/Perpetuum.Server +dotnet run -- --GameRoot "E:\PerpetuumServer2\data" +``` + +CI builds via `.github/workflows/dotnet.yml` — outputs to `bin/x64/Release/net8.0`. + +There are **no automated tests** in this repository. + +## Configuration + +The server reads two config files: + +- **`appsettings.json`** (in `Perpetuum.ServerService2/`) — sets `GameRoot` path and .NET logging +- **`perpetuum.ini`** (inside GameRoot directory) — SQL Server connection string, ports, zone config, feature flags, stored as `GlobalConfiguration` JSON + +Server startup sequence: `Perpetuum.Server` → `PerpetuumBootstrapper.Init(gameRoot)` → loads 18+ Autofac modules → connects to SQL Server → loads entity definitions → `bootstrapper.Start()` → host transitions Init → Starting → Online. + +## Database Source of Truth + +The database structure documentation located under `docs/db_structure` is the authoritative source of truth for all database-related work. + +You MUST always consult these files before: +- generating SQL +- modifying queries +- designing repositories/services +- creating DTOs/models +- writing migrations +- proposing schema changes +- analyzing performance issues +- reasoning about relationships +- generating API contracts involving database entities + +Never assume table structures, column names, relationships, data types, constraints, indexes, views, functions, or stored procedure signatures from memory. + +For every database-related task, begin by identifying which files under `docs/db_structure` are relevant before producing the final answer. + +Before writing any JOIN: +- verify the relationship exists in schema documentation +- identify the exact join keys +- explain the join path internally before generating SQL + +Do not load the entire schema documentation unnecessarily. +Only retrieve and inspect entities directly relevant to the current task. + +### Documentation Structure + +#### Core Schema +- `docs/db_structure/database_schema_documentation.md` + - Contains tables + - Columns + - Relations + - Indexes + - Primary/foreign keys + +#### Stored Procedures +- `docs/db_structure/stored_procedures/*.sql` + - One stored procedure per file + - Filename matches procedure name + +#### Functions +- `docs/db_structure/functions/*.sql` + - One function per file + - Filename matches function name + +#### Views +- `docs/db_structure/views/*.sql` + - One view per file + - Filename matches view name + +#### User-defined data types +- `docs/db_structure/data_types/*.sql` + - One view per file + - Filename matches view name + +### Mandatory Behavior + +When working with database-related tasks: + +1. FIRST search relevant files in `docs/db_structure` +2. THEN generate or analyze code +3. Prefer existing stored procedures/functions/views over inventing new SQL +4. Reuse existing naming conventions and patterns +5. Validate all joins and field names against documentation +6. Never hallucinate schema objects +7. If information is missing from documentation: + - explicitly state what is missing + - avoid guessing + +### SQL Generation Rules + +Before generating SQL: +- verify table existence +- verify column existence +- verify join relationships +- verify parameter names/types for procedures/functions +- verify view definitions + +If an object already exists as: +- stored procedure +- function +- view + +prefer using or extending it instead of duplicating logic. + +### Architecture Rules + +When generating backend code: +- derive models from documented schema +- preserve actual nullability +- preserve actual SQL types +- preserve real relationships +- preserve naming conventions exactly + +Never rename fields unless explicitly requested. + +### Performance Rules + +When analyzing performance: +- inspect indexes from schema docs +- prefer indexed joins/filtering +- avoid assumptions about clustered keys +- check existing views/functions/procedures before proposing alternatives + +### Conflict Resolution + +If generated code conflicts with documentation: +- documentation wins +- do not trust prior conversation context over documentation + +### Output Expectations + +For database-related answers: +- mention which entities were consulted +- reference exact tables/views/procedures/functions used +- explain relationship paths when relevant + +## Architecture + +### Project Layout + +| Project | Role | +|---|---| +| `Perpetuum` | Core library — all game logic | +| `Perpetuum.Bootstrapper` | Autofac DI wiring, one module per subsystem | +| `Perpetuum.RequestHandlers` | 150+ command handler classes | +| `Perpetuum.Server` | Console entry point | +| `Perpetuum.ServerService2` | Windows service wrapper | +| `Perpetuum.ExportedTypes` | Shared type definitions | + +### Dependency Injection (Autofac) + +Everything is wired in `Perpetuum.Bootstrapper/Modules/`. Each major system has its own Autofac module (zones, missions, entities, NPCs, market, etc.). Adding a new service means: implement the class, register it in the appropriate module, and inject it via constructor. + +### Command/Request Handler Pattern + +``` +Client → TCP → Session → Command dispatch → IRequestHandler → Response +``` + +All ~200+ commands are defined in `Commands.cs` with text name, access level, and argument schema. Each command maps to a handler class in `Perpetuum.RequestHandlers/`. To add a new command: define it in `Commands.cs`, create a handler class, register it in the bootstrapper. + +### Entity System + +- `Entity` — base game object with `Eid` (entity ID) and dynamic property bag +- `EntityDefault` — template/definition data loaded from the database +- `OptionalProperty` / `DynamicProperty` — typed property accessors on entity instances +- Entities are not ORM-mapped rows; they carry runtime state overlaid on definition data + +### Zones + +Each `IZone` is a self-contained simulation: terrain grid, units (players/robots/NPCs), environmental effects, locking, and combat. Zones run in parallel. Each zone has its own network listener port. Key files: `src/Perpetuum/Zones/` (35 subdirectories) including `NpcSystem/`, `Terrains/`, `PBS/` (player bases), `Intrusion/`. + +### Modules System + +Robot equipment = `Module` objects with state machines. `ActiveModule.States.cs` manages state transitions. Modules track ammo, energy consumption, and heat. Module types live in `src/Perpetuum/Modules/` (40+ files). + +### Network & Serialization + +- `Perpetuum.Network` — TCP connections with encryption +- `Perpetuum.GenXY` — custom binary wire format; use `GenxyReader`/`GenxyWriter` for protocol I/O, `GenxyConverter` to register custom type serialization + +### Services + +Specialized long-running services under `src/Perpetuum/Services/`: +- `MissionEngine` — mission progression and rewards +- `MarketEngine` — item trading and pricing +- `ProductionEngine` — crafting/manufacturing +- `Sessions` — player session management +- `EventServices` — world events and NPC spawning +- `Standing` — faction relationships + +## Contributing AI Instructions + +### Where to edit + +| You want to change... | Edit this file | +|-----------------------|----------------| +| Project context (this file) | `CLAUDE.md` | +| Architecture deep-dive | `.claude/knowledge/architecture.md` | +| A specialist agent | `.claude/agents/.md` | \ No newline at end of file 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/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..ff9f786 --- /dev/null +++ b/docs/db_structure/database_schema_documentation.md @@ -0,0 +1,6983 @@ +# 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) +- [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]` | + +--- + +## 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/CreateFolderContainer.sql b/docs/db_structure/stored_procedures/CreateFolderContainer.sql new file mode 100644 index 0000000..c806bce --- /dev/null +++ b/docs/db_structure/stored_procedures/CreateFolderContainer.sql @@ -0,0 +1,50 @@ +/****** Object: StoredProcedure [dbo].[CreateFolderContainer] Script Date: 10.05.2026 13:40:59 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[CreateFolderContainer] + @owner BIGINT , + @parent BIGINT , + @result BIGINT OUTPUT + +AS +BEGIN + +DECLARE @folderEid BIGINT + +SET @folderEid = (SELECT dbo.TryGetRandomEid()) + +--create folder def_infinite_capacity_box_container (577) o:967 p:680 h:100 n: q:1 r:False +INSERT dbo.entities + ( eid , + definition , + owner , + parent , + health , + ename , + quantity , + repackaged , + dynprop + ) +VALUES ( @folderEid , -- eid - bigint + 577 , -- definition - int + @owner , -- owner - bigint + @parent , -- parent - bigint + 100 , -- health - float + N'reimbursed items ' + CAST(GETDATE() AS VARCHAR(64)) , -- ename - nvarchar(128) + 1 , -- quantity - int + 0 , -- repackaged - bit + NULL + ) + + --visszamegy az uj eid + SET @result = @folderEid + +RETURN + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/DeleteAllGang.sql b/docs/db_structure/stored_procedures/DeleteAllGang.sql new file mode 100644 index 0000000..76ad6e5 --- /dev/null +++ b/docs/db_structure/stored_procedures/DeleteAllGang.sql @@ -0,0 +1,23 @@ +/****** Object: StoredProcedure [dbo].[DeleteAllGang] Script Date: 10.05.2026 14:02:21 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +-- ============================================= +-- Author: +-- Create date: +-- Description: +-- ============================================= +CREATE PROCEDURE [dbo].[DeleteAllGang] +AS +BEGIN +delete channelmembers where channelid in( select id from channels where type = 3) +delete channels where type = 3 +delete gangmembers +delete gang +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/DuplicateDefinitionConfig.sql b/docs/db_structure/stored_procedures/DuplicateDefinitionConfig.sql new file mode 100644 index 0000000..f0ba094 --- /dev/null +++ b/docs/db_structure/stored_procedures/DuplicateDefinitionConfig.sql @@ -0,0 +1,103 @@ +/****** Object: StoredProcedure [dbo].[DuplicateDefinitionConfig] Script Date: 10.05.2026 15:06:33 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[DuplicateDefinitionConfig] + @sourceDefinition int, + @targetDefinition int +AS +BEGIN + SET NOCOUNT ON; + + INSERT dbo.definitionconfig + ( + definition , + targetdefinition , + summonerscount , + npcpresenceid , + item_work_range , + explosion_radius , + cycle_time , + damage_chemical , + damage_explosive , + damage_kinetic , + damage_thermal , + lifetime , + activationtime , + waves , + missionrelated , + constructionradius , + action_delay , + deploy_radius , + transmitradius , + constructionlevelmax , + blockingradius , + chargeamount , + inconnections , + outconnections , + coretransferred , + transferefficiency , + productionupgradeamount , + productionlevel , + coreconsumption , + effectid , + corecalories , + corekickstartthreshold , + reinforcecountermax , + bandwidthusage , + bandwidthcapacity , + emitradius , + tint , + typeexclusiverange , + network_node_range + ) + SELECT + + @targetDefinition , + targetdefinition , + summonerscount , + npcpresenceid , + item_work_range , + explosion_radius , + cycle_time , + damage_chemical , + damage_explosive , + damage_kinetic , + damage_thermal , + lifetime , + activationtime , + waves , + missionrelated , + constructionradius , + action_delay , + deploy_radius , + transmitradius , + constructionlevelmax , + blockingradius , + chargeamount , + inconnections , + outconnections , + coretransferred , + transferefficiency , + productionupgradeamount , + productionlevel , + coreconsumption , + effectid , + corecalories , + corekickstartthreshold , + reinforcecountermax , + bandwidthusage , + bandwidthcapacity , + emitradius , + tint , + typeexclusiverange , + network_node_range + + FROM dbo.definitionconfig + WHERE definition = @sourceDefinition +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/EpForActivityByType.sql b/docs/db_structure/stored_procedures/EpForActivityByType.sql new file mode 100644 index 0000000..ea22c30 --- /dev/null +++ b/docs/db_structure/stored_procedures/EpForActivityByType.sql @@ -0,0 +1,63 @@ +/****** Object: StoredProcedure [dbo].[EpForActivityByType] Script Date: 10.05.2026 15:10:15 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[EpForActivityByType] + + @topN int = 10, + @daysBack int = 30 +AS +BEGIN + +SET NOCOUNT ON; + +DECLARE @atypes IntList +INSERT @atypes + ( idval ) + SELECT DISTINCT epforactivitytype FROM dbo.epforactivitylog; + +DECLARE @earlier DATETIME, @later DATETIME; +SET @later = GETDATE(); +SET @earlier = DATEADD(DAY,-1 *@daysBack, @later); + +DECLARE @activityType INT; +DECLARE typez CURSOR LOCAL STATIC FORWARD_ONLY FOR SELECT idval FROM @atypes; +OPEN typez; +FETCH NEXT FROM typez INTO @activityType; +WHILE (@@FETCH_STATUS =0) +BEGIN + +SELECT @activityType AS [atype], dbo.activityNameByType(@activityType) AS [typename] + +SELECT TOP (@topN) * FROM +(SELECT SUM(rawpoints) AS [sumpts],epforactivitylog.characterid,characters.nick FROM dbo.epforactivitylog +inner join characters ON characters.characterid = epforactivitylog.characterid +WHERE (eventtime BETWEEN @earlier AND @later) +AND epforactivitytype=@activityType + GROUP BY epforactivitylog.characterid, characters.nick + + ) k + + ORDER BY k.sumpts DESC + + FETCH NEXT FROM typez INTO @activityType; +END +CLOSE typez; DEALLOCATE typez; + + + +SELECT TOP 100 SUM(points) AS [finalpoints],SUM(rawpoints) AS [rawpoints], AVG(boostfactor) AS [avgboost], epforactivitylog.characterid, characters.nick FROM dbo.epforactivitylog +inner join characters ON characters.characterid = epforactivitylog.characterid +WHERE (eventtime BETWEEN @earlier AND @later) +GROUP BY epforactivitylog.characterid, characters.nick +ORDER BY [finalpoints] desc; + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/GetNpcKillEp.sql b/docs/db_structure/stored_procedures/GetNpcKillEp.sql new file mode 100644 index 0000000..30df77b --- /dev/null +++ b/docs/db_structure/stored_procedures/GetNpcKillEp.sql @@ -0,0 +1,19 @@ +/****** Object: StoredProcedure [dbo].[GetNpcKillEp] Script Date: 10.05.2026 16:11:32 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[GetNpcKillEp] + @definition int +AS +BEGIN + SET NOCOUNT ON; + DECLARE @ep INT; + SET @ep = COALESCE((SELECT TOP 1 r.killep FROM dbo.robottemplaterelation r WHERE r.[definition]= @definition ),0 ); + + SELECT @ep; +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountAddCredit.sql b/docs/db_structure/stored_procedures/accountAddCredit.sql new file mode 100644 index 0000000..805cb81 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountAddCredit.sql @@ -0,0 +1,24 @@ +/****** Object: StoredProcedure [dbo].[accountAddCredit] Script Date: 10.05.2026 7:32:12 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountAddCredit] + + @accountId int, + @credit int +AS +BEGIN + SET NOCOUNT ON; + + UPDATE accounts SET credit=credit+@credit WHERE accountID=@accountId; + + + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountAllocateSteamKey.sql b/docs/db_structure/stored_procedures/accountAllocateSteamKey.sql new file mode 100644 index 0000000..bab94a9 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountAllocateSteamKey.sql @@ -0,0 +1,39 @@ +/****** Object: StoredProcedure [dbo].[accountAllocateSteamKey] Script Date: 10.05.2026 7:32:57 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +-- ============================================= +-- Author: +-- Create date: +-- Description: +-- ============================================= +CREATE PROCEDURE [dbo].[accountAllocateSteamKey] + @accountID INT, + @steamID varchar(64) +AS +BEGIN + DECLARE @a int, @keyID int, @steamKey varchar(32) + + SET NOCOUNT ON; + + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + BEGIN TRANSACTION + + IF EXISTS (SELECT id FROM steamkeys WHERE accountid = @accountID OR steamID = @steamID) + BEGIN + COMMIT + RETURN 0 + END + + SELECT TOP 1 @keyID = id, @steamKey = steamkey FROM steamkeys WHERE accountid IS NULL + + UPDATE steamkeys SET accountid = @accountid, assigned = GETDATE(), steamID = @steamID WHERE id = @keyID + + SELECT @keyID as keyID, @steamKey as steamKey + COMMIT + RETURN 1 +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountCreditEnqueue.sql b/docs/db_structure/stored_procedures/accountCreditEnqueue.sql new file mode 100644 index 0000000..75188a2 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountCreditEnqueue.sql @@ -0,0 +1,21 @@ +/****** Object: StoredProcedure [dbo].[accountCreditEnqueue] Script Date: 10.05.2026 7:33:47 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountCreditEnqueue] + @accountId int, + @credit int +AS +BEGIN + SET NOCOUNT ON; + SET TRANSACTION ISOLATION LEVEL READ COMMITTED + BEGIN TRANSACTION + INSERT dbo.accountcreditqueue ( accountid, credit ) VALUES ( @accountId, @credit ) + COMMIT + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountEmailConfirm.sql b/docs/db_structure/stored_procedures/accountEmailConfirm.sql new file mode 100644 index 0000000..3b51b63 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountEmailConfirm.sql @@ -0,0 +1,20 @@ +/****** Object: StoredProcedure [dbo].[accountEmailConfirm] Script Date: 10.05.2026 7:34:40 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountEmailConfirm] + @accountID int + +AS +BEGIN + SET NOCOUNT ON; + + UPDATE accounts SET emailConfirmed=1 WHERE accountID=@accountID + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPackageBought.sql b/docs/db_structure/stored_procedures/accountPackageBought.sql new file mode 100644 index 0000000..572d3dc --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPackageBought.sql @@ -0,0 +1,31 @@ +/****** Object: StoredProcedure [dbo].[accountPackageBought] Script Date: 10.05.2026 7:36:53 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountPackageBought] + + @accountId int, + @packageId int +AS +BEGIN + SET NOCOUNT ON; + + -- check package existence + IF EXISTS (SELECT 1 FROM dbo.accountpremiumpackages WHERE accountid=@accountId AND packageid=@packageId) + BEGIN + RETURN; + END + + + --log the purchase + INSERT dbo.accountpremiumpackages ( accountid, packageid ) VALUES ( @accountId, @packageId ); + + --return result + SELECT 1 AS result; + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPackageGenerateAll.sql b/docs/db_structure/stored_procedures/accountPackageGenerateAll.sql new file mode 100644 index 0000000..558885c --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPackageGenerateAll.sql @@ -0,0 +1,43 @@ +/****** Object: StoredProcedure [dbo].[accountPackageGenerateAll] Script Date: 10.05.2026 7:37:29 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountPackageGenerateAll] + @accountId int +AS +BEGIN + + SET NOCOUNT ON; + + DECLARE @packIdList integer_list_tbltype, @packageName VARCHAR(512);; + + INSERT @packIdList ( n ) VALUES + ( 1 ), ( 2 ), ( 3 ), ( 4 ), ( 5 ); + + DECLARE @packId INT; + DECLARE packIds CURSOR LOCAL FORWARD_ONLY READ_ONLY FOR SELECT n FROM @packIdList; + OPEN packIds + FETCH NEXT FROM packIds INTO @packId; + WHILE( @@FETCH_STATUS =0) + BEGIN + SET @packageName = (SELECT name FROM dbo.packages WHERE id=@packId); + PRINT ('processing package:' + CAST(@packId AS VARCHAR(20)) + ' ' + @packageName); + + IF (dbo.accountPackageIsPurchased(@accountId, @packId)=1) + BEGIN + EXEC dbo.accountPackageProcessOne @accountId, @packId; + END + + FETCH NEXT FROM packIds INTO @packId; + END + + CLOSE packIds; DEALLOCATE packIds; + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPackageGenerateSpark.sql b/docs/db_structure/stored_procedures/accountPackageGenerateSpark.sql new file mode 100644 index 0000000..133480e --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPackageGenerateSpark.sql @@ -0,0 +1,58 @@ +/****** Object: StoredProcedure [dbo].[accountPackageGenerateSpark] Script Date: 10.05.2026 7:38:14 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountPackageGenerateSpark] + + @accountId int, + @sparkActivatorDefinition int +AS +BEGIN + + SET NOCOUNT ON; + + DECLARE @defName VARCHAR(512); + SET @defName = dbo.GetDefinitionName(@sparkActivatorDefinition); + + DECLARE @nofCurrentRedeemableSpark INT; + IF EXISTS ( SELECT 1 FROM dbo.accountredeemableitems WHERE accountid=@accountId AND [definition]=@sparkActivatorDefinition AND wasredeemed=0) + BEGIN + -- has unredeemed + PRINT (' accountId:' + CAST(@accountId AS VARCHAR(20)) + ' has unredeemed sparkActivator:' + CAST(@sparkActivatorDefinition AS VARCHAR(20)) + ' ' + @defName); + RETURN + + END + + + DECLARE @activeCharacters integer_list_tbltype, @nofChararcters INT; + + INSERT @activeCharacters ( n ) + SELECT characterID FROM characters WHERE accountID=@accountId AND active=1; + + SET @nofChararcters = (SELECT COUNT(*) FROM @activeCharacters); + + DECLARE @nofActivated INT; + SET @nofActivated = (SELECT COUNT(*) FROM dbo.accountredeemableitems WHERE accountid=@accountId AND [definition]=@sparkActivatorDefinition AND characterid IN (SELECT n FROM @activeCharacters)); + + PRINT('activations:' + CAST(@nofActivated AS VARCHAR(20)) + ' characters:' + CAST(@nofChararcters AS VARCHAR(20))); + + IF (@nofChararcters > @nofActivated OR @nofChararcters=0) + BEGIN + + INSERT dbo.accountredeemableitems + ( accountid , [definition] , quantity ) + VALUES ( @accountId, @sparkActivatorDefinition, 1 ) + + PRINT ( 'spark activator inserted for accountId:'+CAST(@accountId AS VARCHAR(20)) + ' - '+ CAST(@sparkActivatorDefinition AS VARCHAR(20)) + ' ' + @defName ); + RETURN; + END + + + + PRINT( 'enough activators for accountId:'+CAST(@accountId AS VARCHAR(20)) + ' - '+ CAST(@sparkActivatorDefinition AS VARCHAR(20)) + ' ' + @defName ); +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPackageHas.sql b/docs/db_structure/stored_procedures/accountPackageHas.sql new file mode 100644 index 0000000..6bc125c --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPackageHas.sql @@ -0,0 +1,20 @@ +/****** Object: StoredProcedure [dbo].[accountPackageHas] Script Date: 10.05.2026 7:39:06 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountPackageHas] + + @accountId int, + @packageId int +AS +BEGIN + SET NOCOUNT ON; + + SELECT dbo.accountPackageIsPurchased(@accountId,@packageId) AS haspackage; + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPackageList.sql b/docs/db_structure/stored_procedures/accountPackageList.sql new file mode 100644 index 0000000..b780de7 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPackageList.sql @@ -0,0 +1,20 @@ +/****** Object: StoredProcedure [dbo].[accountPackageList] Script Date: 10.05.2026 7:40:16 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountPackageList] + + @accountId int + +AS +BEGIN + SET NOCOUNT ON; + + SELECT * FROM dbo.accountpremiumpackages WHERE accountid=@accountId + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPackageProcessOne.sql b/docs/db_structure/stored_procedures/accountPackageProcessOne.sql new file mode 100644 index 0000000..182c705 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPackageProcessOne.sql @@ -0,0 +1,57 @@ +/****** Object: StoredProcedure [dbo].[accountPackageProcessOne] Script Date: 10.05.2026 7:40:53 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[accountPackageProcessOne] + @accountId int, + @packageId int +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @iDefName VARCHAR(512), @packageName VARCHAR(512); + DECLARE @iDef INT, @iQty INT; + + SET @packageName = (SELECT name FROM dbo.packages WHERE id=@packageId); + + DECLARE packItems CURSOR LOCAL FORWARD_ONLY READ_ONLY FOR SELECT [definition],[quantity] FROM dbo.packageitems WHERE packageid = @packageId; + OPEN packItems; + FETCH NEXT FROM packItems INTO @iDef, @iQty; + WHILE(@@FETCH_STATUS =0) + BEGIN + + SET @iDefName = dbo.GetDefinitionName(@iDef); + --PRINT ( @iDefName + ' ' + CAST(@iQty AS VARCHAR(20))); + + IF (dbo.isDefinitionSparkUnlocker(@iDef)=0) + BEGIN + -- not a spark unlocker + IF (dbo.accountPackageHasItem(@accountId,@iDef,@iQty, @packageId)=0) + BEGIN + + --has no item yet + INSERT dbo.accountredeemableitems + ( accountid , [definition] , quantity, packageid ) + VALUES ( @accountId, @iDef, @iQty, @packageId ) + + PRINT ( 'accountId:' + CAST(@accountId AS VARCHAR(20)) + ' has no item for package: ' + @packageName + ' item:' + @iDefName + ' qty:' + CAST(@iQty AS VARCHAR(20))); + END + END + ELSE + BEGIN + + PRINT ('item is a spark unlocker: ' + @iDefName); + EXEC dbo.accountPackageGenerateSpark @accountId, @iDef + END + + + FETCH NEXT FROM packItems INTO @iDef, @iQty; + END + CLOSE packItems; DEALLOCATE packItems; + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountPurchase.sql b/docs/db_structure/stored_procedures/accountPurchase.sql new file mode 100644 index 0000000..1ecf82d --- /dev/null +++ b/docs/db_structure/stored_procedures/accountPurchase.sql @@ -0,0 +1,53 @@ +/****** Object: StoredProcedure [dbo].[accountPurchase] Script Date: 10.05.2026 7:41:37 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + + +CREATE PROCEDURE [dbo].[accountPurchase] + @accountID int + + +AS +BEGIN tran + + SET NOCOUNT ON; + + DECLARE @alreadyActive BIT + + SET @alreadyActive = (SELECT isactive FROM accounts WHERE accountID=@accountID) + + IF (@alreadyActive = 0) + BEGIN + --First purchase activates account and adds starting EP + + UPDATE accounts SET isactive=1 WHERE accountID=@accountID + + --works from packages + --EXEC dbo.extensionPointsInject @accountID, 40160 + + -- add standard package + EXEC dbo.accountPackageBought @accountID, 1 + + + END + + + +commit + + --send ok... + select 1 + return 1 + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountSimpleDelete.sql b/docs/db_structure/stored_procedures/accountSimpleDelete.sql new file mode 100644 index 0000000..9bbd46a --- /dev/null +++ b/docs/db_structure/stored_procedures/accountSimpleDelete.sql @@ -0,0 +1,30 @@ +/****** Object: StoredProcedure [dbo].[accountSimpleDelete] Script Date: 10.05.2026 7:42:21 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +--Delete an account. Used from MCP. + +CREATE PROCEDURE [dbo].[accountSimpleDelete] + + @accountID int + +AS +BEGIN transaction + SET NOCOUNT ON; + + --disconnect character from account + update characters set accountID=null where accountID=@accountID + + --delete from accounts + delete accounts where accountID=@accountID + +commit + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountonlinetimestart.sql b/docs/db_structure/stored_procedures/accountonlinetimestart.sql new file mode 100644 index 0000000..8e4f520 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountonlinetimestart.sql @@ -0,0 +1,41 @@ +/****** Object: StoredProcedure [dbo].[accountonlinetimestart] Script Date: 10.05.2026 7:35:13 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountonlinetimestart] + + @accountID INT, + @ip VARCHAR(50), + @hwHash VARCHAR(50) = NULL, + @isTrial bit + +AS +BEGIN + SET NOCOUNT ON; + + IF EXISTS (SELECT accountid FROM dbo.accountonlinetime WHERE accountid=@accountID AND loggedout IS NULL) + BEGIN + UPDATE dbo.accountonlinetime + SET loggedout = getdate() + WHERE accountid=@accountID AND loggedout IS NULL + END + + + + INSERT dbo.accountonlinetime (accountid,ip,hwhash,istrial) VALUES + ( + @accountID, + @ip, + @hwHash, + @isTrial + ) + + +END + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/accountonlinetimestop.sql b/docs/db_structure/stored_procedures/accountonlinetimestop.sql new file mode 100644 index 0000000..64b4f72 --- /dev/null +++ b/docs/db_structure/stored_procedures/accountonlinetimestop.sql @@ -0,0 +1,25 @@ +/****** Object: StoredProcedure [dbo].[accountonlinetimestop] Script Date: 10.05.2026 7:36:01 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[accountonlinetimestop] + + @accountID INT, + @safeLogOut BIT + +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.accountonlinetime + SET loggedout=GETDATE(),safelogout=@safeLogOut + WHERE accountid=@accountID AND loggedout IS NULL + + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addCampaignGoodiePack.sql b/docs/db_structure/stored_procedures/addCampaignGoodiePack.sql new file mode 100644 index 0000000..f551af1 --- /dev/null +++ b/docs/db_structure/stored_procedures/addCampaignGoodiePack.sql @@ -0,0 +1,40 @@ +/****** Object: StoredProcedure [dbo].[addCampaignGoodiePack] Script Date: 10.05.2026 7:42:59 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[addCampaignGoodiePack] + + @accountID int , + @campaignToken VARCHAR(128) +AS +BEGIN + + SET NOCOUNT ON; + + DECLARE @campaignId INT + + SET @campaignId = (SELECT id FROM dbo.campaigns WHERE campaigntoken = @campaignToken) + + IF (@campaignId IS NULL OR @campaignId = 0) + BEGIN + + SELECT -2, 'campaign was not found' + RETURN + END + + IF EXISTS (SELECT accountid FROM accountcampaignitems WHERE accountid=@accountID AND campaignid=@campaignId) + BEGIN + SELECT -1,'pack already exists' + RETURN + END + + INSERT dbo.accountcampaignitems (accountid,campaignid) + VALUES ( @accountID,@campaignId) + + SELECT 0,'success' +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addDefinitionToBetaMarkets.sql b/docs/db_structure/stored_procedures/addDefinitionToBetaMarkets.sql new file mode 100644 index 0000000..82e0a98 --- /dev/null +++ b/docs/db_structure/stored_procedures/addDefinitionToBetaMarkets.sql @@ -0,0 +1,61 @@ +/****** Object: StoredProcedure [dbo].[addDefinitionToBetaMarkets] Script Date: 10.05.2026 7:43:47 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[addDefinitionToBetaMarkets] + @definition INT, + @price FLOAT +AS +BEGIN + +SET NOCOUNT ON; + +DECLARE @marketEid BIGINT + +DECLARE TableCursor CURSOR FOR SELECT eid FROM dbo.getLiveBetaMarkets() + +OPEN TableCursor + +FETCH NEXT FROM TableCursor INTO @marketEid +WHILE @@FETCH_STATUS = 0 +BEGIN + INSERT dbo.marketitems + ( + marketeid , + itemdefinition , + submittereid, + duration , + isSell , + price , + quantity , + isvendoritem + + ) + VALUES ( + @marketEid , -- marketeid - bigint + @definition , -- itemdefinition - int + 90 , -- submittereid - bigint + 0 , -- duration - int + 1 , -- isSell - bit + @price , -- price - float + -1 , -- quantity - int + 1 -- isvendoritem - bit + + ) + + + + + FETCH NEXT FROM TableCursor INTO @marketEid +END + +CLOSE TableCursor + +DEALLOCATE TableCursor + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addDefinitionToDefaultMarkets.sql b/docs/db_structure/stored_procedures/addDefinitionToDefaultMarkets.sql new file mode 100644 index 0000000..682ecb8 --- /dev/null +++ b/docs/db_structure/stored_procedures/addDefinitionToDefaultMarkets.sql @@ -0,0 +1,61 @@ +/****** Object: StoredProcedure [dbo].[addDefinitionToDefaultMarkets] Script Date: 10.05.2026 7:44:34 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +create PROCEDURE [dbo].[addDefinitionToDefaultMarkets] + @definition INT, + @price FLOAT +AS +BEGIN + +SET NOCOUNT ON; + +DECLARE @marketEid BIGINT + +DECLARE TableCursor CURSOR FOR SELECT eid FROM dbo.getLiveDefaultMarkets() + +OPEN TableCursor + +FETCH NEXT FROM TableCursor INTO @marketEid +WHILE @@FETCH_STATUS = 0 +BEGIN + INSERT dbo.marketitems + ( + marketeid , + itemdefinition , + submittereid, + duration , + isSell , + price , + quantity , + isvendoritem + + ) + VALUES ( + @marketEid , -- marketeid - bigint + @definition , -- itemdefinition - int + 90 , -- submittereid - bigint + 0 , -- duration - int + 1 , -- isSell - bit + @price , -- price - float + -1 , -- quantity - int + 1 -- isvendoritem - bit + + ) + + + + + FETCH NEXT FROM TableCursor INTO @marketEid +END + +CLOSE TableCursor + +DEALLOCATE TableCursor + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addIP.sql b/docs/db_structure/stored_procedures/addIP.sql new file mode 100644 index 0000000..2fe900a --- /dev/null +++ b/docs/db_structure/stored_procedures/addIP.sql @@ -0,0 +1,54 @@ +/****** Object: StoredProcedure [dbo].[addIP] Script Date: 10.05.2026 7:45:16 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[addIP] + + ( + @ip varchar(16), + @clientID int, + @banreleaseseconds int + ) + +AS + SET NOCOUNT ON + + declare @bantime smalldatetime + + begin tran + + set @bantime = (select top(1) bantime from connectedips where banned=1 and ipaddress=@ip) + + if not (@bantime is NULL) + begin + + --ip was banned once + if ( datediff (second, @bantime, getdate()) > @banReleaseSeconds) + begin + --release the ban + delete connectedips where banned=1 and ipaddress=@ip + + end + else + begin + --still banned: exit + select -2 --ip was banned + commit + return + end + + end + + --ip is not present in the table: insert + insert connectedips (ipaddress,clientid) values (@ip,@clientID) + + select 0 + + commit + + RETURN + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addKill.sql b/docs/db_structure/stored_procedures/addKill.sql new file mode 100644 index 0000000..57f3cb1 --- /dev/null +++ b/docs/db_structure/stored_procedures/addKill.sql @@ -0,0 +1,58 @@ +/****** Object: StoredProcedure [dbo].[addKill] Script Date: 10.05.2026 7:45:58 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[addKill] + @characterID int = 0, + @killedPlayers int = 0, + @killedNPCs int = 0, + @startDate datetime, + @endDate datetime + + +AS +BEGIN + SET NOCOUNT ON; + + + declare @date as datetime + set @date = convert(char(8),getdate(),112) + + if not exists (select characterid from characterhighscore where characterid=@characterID and date=@date) + begin + insert characterhighscore (characterid,playerskilled,npcskilled,date) values + (@characterID, @killedPlayers, @killedNPCs,@date ) + + end + else + begin + update characterhighscore set + playerskilled = playerskilled + @killedPlayers, + npcskilled = npcskilled + @killedNPCs + + where characterID=@characterID and date = @date + end + + + select characterid,sum(playerskilled) as pk + from characterhighscore + where + characterid=@characterID and + (date between @startDate and @endDate) + group by characterid order by pk desc + +END + + + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addVendorBuyItem.sql b/docs/db_structure/stored_procedures/addVendorBuyItem.sql new file mode 100644 index 0000000..0222623 --- /dev/null +++ b/docs/db_structure/stored_procedures/addVendorBuyItem.sql @@ -0,0 +1,82 @@ +/****** Object: StoredProcedure [dbo].[addVendorBuyItem] Script Date: 10.05.2026 7:47:54 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[addVendorBuyItem] + + @vendoreid bigint, + @definition int, + @price float = 0, + @amount int = 0 + +AS +BEGIN + SET NOCOUNT ON; + + declare @vendorbuyprofit float, @marketEID bigint, @baseEID bigint, @definitionName varchar(128) + + -- check definition + if not exists (select definitionname from entitydefaults where definition=@definition) + begin + select N'definition not exists' as result + return + end + + -- check purchasable + if (select purchasable from entitydefaults where definition=@definition) = 0 + begin + select N'definition not purchasable' as result + return + end + + + + -- get buy profit + set @vendorbuyprofit = (select vendorbuyprofit from vendors where vendorEID=@vendoreid) + if @vendorbuyprofit is NULL + begin + select N'vendor not exists' as result + return + end + + -- get definition name + set @definitionName = (select definitionname from entitydefaults where definition=@definition) + + if (@price = 0) + begin + set @price = dbo.calcPrice(@definition) -- get price from itemprices + if (@price = 0) + begin + select N'item price not defined.' as result + return + end + end + + set @price = @price * @vendorbuyprofit + + if (@amount = 0) + begin + set @amount = -1 --force infinite amount + end + + set @baseEID = (select parent from entities where eid=@vendoreid) + + -- definition = 10 : market + set @marketEID = (select eid from entities where parent=@baseEID and definition=10) + + insert marketitems (marketeid,itemdefinition,submittereid,isSell,price,quantity,isvendoritem) + values + (@marketEID,@definition,@vendoreid,0,@price,@amount,1) + + SELECT (N'buy order added. \o/ definition:' + cast(@definition as varchar(20)) + + N' ' + @definitionName + N' price:' + cast(@price as varchar(20)) + + N' amount:' + cast(@amount as varchar(20))) as result +END + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addVendorSellItem.sql b/docs/db_structure/stored_procedures/addVendorSellItem.sql new file mode 100644 index 0000000..fd381ca --- /dev/null +++ b/docs/db_structure/stored_procedures/addVendorSellItem.sql @@ -0,0 +1,83 @@ +/****** Object: StoredProcedure [dbo].[addVendorSellItem] Script Date: 10.05.2026 7:48:32 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[addVendorSellItem] + + @vendoreid bigint, + @definition int, + @price bigint = 0, + @amount int = 0 + +AS +BEGIN + SET NOCOUNT ON; + + declare @vendorsellprofit float, @marketEID bigint, @baseEID bigint, @definitionName varchar(128) + + -- check definition + if not exists (select definitionname from entitydefaults where definition=@definition) + begin + select N'definition not exists' as result + return + end + +/* + -- check purchasable + if (select purchasable from entitydefaults where definition=@definition) = 0 + begin + select N'definition not purchasable' as result + return + end +*/ + + + + -- get sell profit + set @vendorsellprofit = (select vendorsellprofit from vendors where vendorEID=@vendoreid) + if @vendorsellprofit is NULL + begin + select N'vendor not exists' as result + return + end + + -- get definition name + set @definitionName = (select definitionname from entitydefaults where definition=@definition) + + if (@price = 0) + begin + set @price = dbo.calcPrice(@definition) -- get price from itemprices + if (@price = 0) + begin + select N'item price not defined.' as result + return + end + end + + set @price = @price * @vendorsellprofit + + if (@amount = 0) + begin + set @amount = -1 --force infinite amount + end + + set @baseEID = (select parent from entities where eid=@vendoreid) + + -- definition = 10 : market + set @marketEID = (select eid from entities where parent=@baseEID and definition=10) + + insert marketitems (marketeid,itemdefinition,submittereid,isSell,price,quantity,isvendoritem) + values + (@marketEID,@definition,@vendoreid,1,@price,@amount,1) + + SELECT (N'sell order added. \o/ definition:' + cast(@definition as varchar(20)) + + N' ' + @definitionName + N' price:' + cast(@price as varchar(20)) + + N' amount:' + cast(@amount as varchar(20))) as result +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addownerincome.sql b/docs/db_structure/stored_procedures/addownerincome.sql new file mode 100644 index 0000000..4f28af2 --- /dev/null +++ b/docs/db_structure/stored_procedures/addownerincome.sql @@ -0,0 +1,34 @@ +/****** Object: StoredProcedure [dbo].[addownerincome] Script Date: 10.05.2026 7:46:40 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[addownerincome] + + @corpEID bigint, + @amount float +AS +BEGIN + + + SET NOCOUNT ON; + + if exists (select amount from ownerincome where corporationeid=@corpEID) + begin + update ownerincome set amount=amount+@amount where corporationeid=@corpEID + end + else + begin + insert ownerincome (corporationeid,amount) values (@corpEID,@amount) + end + + + + +END + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/addpresettorandompool.sql b/docs/db_structure/stored_procedures/addpresettorandompool.sql new file mode 100644 index 0000000..f261acf --- /dev/null +++ b/docs/db_structure/stored_procedures/addpresettorandompool.sql @@ -0,0 +1,23 @@ +/****** Object: StoredProcedure [dbo].[addpresettorandompool] Script Date: 10.05.2026 7:47:17 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[addpresettorandompool] + + @presetID int, + @presenceID int +AS +BEGIN + + + SET NOCOUNT ON; + + INSERT npcrandomflockpool (presenceid, flockid, rate) + SELECT @presenceID,flockid,rate FROM npcpoolpresetvalues WHERE presetid=@presetID + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/artifactReset.sql b/docs/db_structure/stored_procedures/artifactReset.sql new file mode 100644 index 0000000..973d357 --- /dev/null +++ b/docs/db_structure/stored_procedures/artifactReset.sql @@ -0,0 +1,22 @@ +/****** Object: StoredProcedure [dbo].[artifactReset] Script Date: 10.05.2026 7:49:10 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[artifactReset] + +AS +BEGIN + + SET NOCOUNT ON; + DECLARE @fromDate DATETIME; +SET @fromDate= DATEADD(DAY,-3,GETDATE()); +SELECT @fromDate; + +DELETE dbo.artifacts WHERE created<@fromDate + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/backupCurrentDbFull.sql b/docs/db_structure/stored_procedures/backupCurrentDbFull.sql new file mode 100644 index 0000000..f2879cf --- /dev/null +++ b/docs/db_structure/stored_procedures/backupCurrentDbFull.sql @@ -0,0 +1,32 @@ +/****** Object: StoredProcedure [dbo].[backupCurrentDbFull] Script Date: 10.05.2026 13:17:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[backupCurrentDbFull] + @note VARCHAR(128) = '' +AS +BEGIN + + +DECLARE @filename VARCHAR(512), @dayString VARCHAR(64), @currentDb VARCHAR(512), @logicalName VARCHAR(512); +SET @currentDb = DB_NAME() +SET @dayString = dbo.getdaystring(GETDATE()) +SET @filename = @currentDb +'_'+ @dayString + '_' + @note + '.bak' +SET @logicalName = @currentDb + '__' + @dayString +PRINT @filename + +--delayed transactions, flush log +EXEC sys.sp_flush_log + +BACKUP DATABASE @currentDb TO +DISK = @filename +WITH +NOFORMAT, NOINIT, SKIP, STATS = 7, BUFFERCOUNT = 256, MAXTRANSFERSIZE = 4194304, +NAME = @logicalName + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/backupLog.sql b/docs/db_structure/stored_procedures/backupLog.sql new file mode 100644 index 0000000..b29dd54 --- /dev/null +++ b/docs/db_structure/stored_procedures/backupLog.sql @@ -0,0 +1,40 @@ +/****** Object: StoredProcedure [dbo].[backupLog] Script Date: 10.05.2026 13:19:17 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[backupLog] + +AS +BEGIN + +DECLARE @filename VARCHAR(512), @file VARCHAR(512), @dayString VARCHAR(64), @currentDb VARCHAR(512); +SET @currentDb = DB_NAME() +SET @dayString = dbo.getdaystring(GETDATE()) +SET @filename = @currentDb + '_' + @dayString + '_LOG' +SET @file = @filename + '.bak'; + +EXEC sys.sp_flush_log +print 'log flushed. ' + @file + ' log backup starts.' + +--NOFORMAT: don't format media +--NOINIT+SKIP: append or create the backups +--NAME: logical name +--STATS: progress display at every % +--BUFFERCOUNT: how many buffers +--MAXTRANSFERSIZE: the largest unit of transfer in bytes to be used between SQL Server and the backup media. +--total memory: buffercount * maxtransfersize. +BACKUP LOG @currentDb +TO DISK = @file +WITH NOFORMAT, NOINIT, SKIP, NAME = N'logbackup', STATS = 4, BUFFERCOUNT = 64, MAXTRANSFERSIZE = 4194304 + +DBCC SQLPERF (LOGSPACE) +--DBCC LOGINFO; + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/centralBank_add.sql b/docs/db_structure/stored_procedures/centralBank_add.sql new file mode 100644 index 0000000..6bd6428 --- /dev/null +++ b/docs/db_structure/stored_procedures/centralBank_add.sql @@ -0,0 +1,38 @@ +/****** Object: StoredProcedure [dbo].[centralBank_add] Script Date: 10.05.2026 13:19:59 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[centralBank_add] + ( + @amount float, + @transactionType int + ) + /* + This stored procedure has to update all the incoming cash flow statistics/calculations + */ + +AS + SET NOCOUNT ON + + + declare @currentBankCredit bigint + + set @currentBankCredit = (select top(1) bankcredit from gameglobals) + + set @currentBankCredit = @currentBankCredit + cast(@amount as bigint) + + --insert log + insert centralbanktransactions (eventtime,transactiontype,amount,bankcredit) values (getdate(),@transactionType, @amount, @currentBankCredit) + + --increase central bank's money + update gameglobals set bankcredit = @currentBankCredit + + + + RETURN + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/centralBank_addLog.sql b/docs/db_structure/stored_procedures/centralBank_addLog.sql new file mode 100644 index 0000000..0f22c26 --- /dev/null +++ b/docs/db_structure/stored_procedures/centralBank_addLog.sql @@ -0,0 +1,31 @@ +/****** Object: StoredProcedure [dbo].[centralBank_addLog] Script Date: 10.05.2026 13:20:45 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[centralBank_addLog] + @day smalldatetime + +AS +BEGIN + + SET NOCOUNT ON + + declare @credit bigint + set @credit = (select top(1) bankcredit from gameglobals) + + if not exists (select amount from centralbanklog where eventday=@day) + begin + insert centralbanklog ([eventday],amount) values (@day,@credit) + end + else + begin + update centralbanklog set amount=@credit where eventday=@day + end + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/centralBank_sub.sql b/docs/db_structure/stored_procedures/centralBank_sub.sql new file mode 100644 index 0000000..cf592b2 --- /dev/null +++ b/docs/db_structure/stored_procedures/centralBank_sub.sql @@ -0,0 +1,41 @@ +/****** Object: StoredProcedure [dbo].[centralBank_sub] Script Date: 10.05.2026 13:21:30 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[centralBank_sub] + ( + @amount float, + @transactionType int + ) + /* deal with statistics */ + +AS + SET NOCOUNT ON + + declare @currentBankCredit bigint + set @currentBankCredit = (select top(1) bankcredit from gameglobals) + + set @currentBankCredit = @currentBankCredit - cast(@amount as bigint) + + + if @currentBankCredit < 0 + begin + -- trigger function here that the central bank gave loan to itself => 1B + set @currentBankCredit = @currentBankCredit + 1000000000 + end + + --insert log + insert centralbanktransactions (eventtime,transactiontype,amount,bankcredit) values (getdate(),@transactionType,-1 * @amount, @currentBankCredit) + + --increase central bank's money + update gameglobals set bankcredit = @currentBankCredit + + RETURN + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/changeEIDinOneTable.sql b/docs/db_structure/stored_procedures/changeEIDinOneTable.sql new file mode 100644 index 0000000..532a6d0 --- /dev/null +++ b/docs/db_structure/stored_procedures/changeEIDinOneTable.sql @@ -0,0 +1,32 @@ +/****** Object: StoredProcedure [dbo].[changeEIDinOneTable] Script Date: 10.05.2026 13:22:12 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[changeEIDinOneTable] + + @sourceEID bigint, + @targetEID BIGINT, + @tableName VARCHAR(128), + @columnName VARCHAR(128) +AS +BEGIN + + DECLARE @statement NVARCHAR(500), @Parameters NVARCHAR(500) + + SET @statement = + N'IF EXISTS (SELECT * FROM ' + @tableName + ' WHERE ' + @columnName + '=@sourceEID) + BEGIN + UPDATE ' + @tableName + ' SET '+@columnName+'=@targetEID WHERE '+@columnName+'=@sourceEID + END' + + SET @Parameters = N'@sourceEID bigint, @targetEID bigint' + + EXEC sp_executesql @statement, @Parameters, @sourceEID, @targetEID + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/changeFacilities.sql b/docs/db_structure/stored_procedures/changeFacilities.sql new file mode 100644 index 0000000..719fad3 --- /dev/null +++ b/docs/db_structure/stored_procedures/changeFacilities.sql @@ -0,0 +1,111 @@ +/****** Object: StoredProcedure [dbo].[changeFacilities] Script Date: 10.05.2026 13:22:48 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[changeFacilities] + @baseName VARCHAR(50), + @facilityLevels VARCHAR(max) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @baseEid BIGINT + SET @baseEid = (SELECT eid FROM dbo.entities WHERE ename=@baseName) + + IF (@baseEid IS NULL) + BEGIN + SELECT 'ERROR!! no eid for name: ' , @baseName + RETURN + end + + DECLARE @facilityEid BIGINT, @newDef INT, @pattern VARCHAR(50) + + DECLARE @id int, @value VARCHAR(max) + DECLARE levels CURSOR LOCAL STATIC FORWARD_ONLY FOR SELECT id,value FROM dbo.splitString(@facilityLevels,',') + OPEN levels + FETCH NEXT FROM levels INTO @id,@value + WHILE @@FETCH_STATUS = 0 + BEGIN + + SET @facilityEid = NULL; + SET @newDef = NULL; + + IF (@value IS NULL) + BEGIN + SELECT 'ERROR!!! level is null.' + RETURN + END + + + IF (@id=1) + BEGIN + SET @pattern = 'refinery'; + SET @facilityEid = dbo.productionFacilityFromBaseByPattern(@baseEid,@pattern); + SET @newDef = dbo.productionFacilityByLevel(@pattern,@value) + end + ELSE IF (@id=2) + BEGIN + SET @pattern = 'research'; + SET @newDef = dbo.productionFacilityByLevel(@pattern,@value) + SET @facilityEid = dbo.productionFacilityFromBaseByPattern(@baseEid,@pattern); + end + ELSE IF (@id=3) + BEGIN + SET @pattern = 'reprocessor'; + SET @newDef = dbo.productionFacilityByLevel(@pattern,@value) + SET @facilityEid = dbo.productionFacilityFromBaseByPattern(@baseEid,@pattern); + end + ELSE IF (@id=4) + BEGIN + SET @pattern = 'repair'; + SET @newDef = dbo.productionFacilityByLevel(@pattern,@value) + SET @facilityEid = dbo.productionFacilityFromBaseByPattern(@baseEid,@pattern); + end + ELSE IF (@id=5) + BEGIN + SET @pattern = 'mill'; + SET @newDef = dbo.productionFacilityByLevel(@pattern,@value) + SET @facilityEid = dbo.productionFacilityFromBaseByPattern(@baseEid,@pattern); + end + ELSE IF (@id=6) + BEGIN + SET @pattern = 'prototyper'; + SET @newDef = dbo.productionFacilityByLevel(@pattern,@value) + SET @facilityEid = dbo.productionFacilityFromBaseByPattern(@baseEid,@pattern); + end + + IF (@facilityEid IS NULL) + BEGIN + SELECT 'ERROR!! no facility', @pattern, 'base', @baseEid + END + + IF (@newDef IS NULL) + BEGIN + SELECT 'ERROR! no new definition', @pattern + END + + IF (@newDef IS NOT NULL AND @facilityEid IS NOT NULL) + BEGIN + + --display + SELECT eid, dbo.GetDefinitionName([definition]), '=>' , dbo.GetDefinitionName(@newDef) FROM dbo.entities WHERE eid=@facilityEid + + -- the actual work + --UPDATE dbo.entities SET [definition]=@newDef WHERE eid=@facilityEid; + + + END + + + FETCH NEXT FROM levels INTO @id,@value + END + CLOSE levels;DEALLOCATE levels + + + SELECT ' ' +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/channelAddBan.sql b/docs/db_structure/stored_procedures/channelAddBan.sql new file mode 100644 index 0000000..3ada6d5 --- /dev/null +++ b/docs/db_structure/stored_procedures/channelAddBan.sql @@ -0,0 +1,22 @@ +/****** Object: StoredProcedure [dbo].[channelAddBan] Script Date: 10.05.2026 13:23:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[channelAddBan] +( + @memberid as int, + @channelid as int +) +AS +BEGIN + +if not exists (select id from channelbans where memberid = @memberid and channelid = @channelid) +begin + insert into channelbans (memberid,channelid) values (@memberid,@channelid) +end + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/channelMemberAdd.sql b/docs/db_structure/stored_procedures/channelMemberAdd.sql new file mode 100644 index 0000000..cc12767 --- /dev/null +++ b/docs/db_structure/stored_procedures/channelMemberAdd.sql @@ -0,0 +1,25 @@ +/****** Object: StoredProcedure [dbo].[channelMemberAdd] Script Date: 10.05.2026 13:25:18 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[channelMemberAdd] +( + @channelid as int, + @memberid as int, + @role as int +) +AS +BEGIN + +if not exists (select id from channelmembers where channelid = @channelid and memberid = @memberid) +begin + insert into channelmembers (channelid,memberid,[role]) values (@channelid,@memberId,@role) +end + +select @@ROWCOUNT + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/characterDockToTMA.sql b/docs/db_structure/stored_procedures/characterDockToTMA.sql new file mode 100644 index 0000000..e434673 --- /dev/null +++ b/docs/db_structure/stored_procedures/characterDockToTMA.sql @@ -0,0 +1,31 @@ +/****** Object: StoredProcedure [dbo].[characterDockToTMA] Script Date: 10.05.2026 13:26:59 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[characterDockToTMA] + @characterId int + +AS +BEGIN + SET NOCOUNT ON; + DECLARE @baseEid BIGINT,@publicContainer BIGINT, @activeRobot BIGINT + SET @baseEid = 561 + SET @publicContainer = 680 + + --dock character to tma + UPDATE characters SET docked=1,zoneID=NULL,positionX=NULL,positionY=NULL,baseEID=@baseEid WHERE characterID=@characterId + + SET @activeRobot = (SELECT activechassis FROM dbo.characters WHERE characterID=@characterId) + + IF (@activeRobot IS NOT NULL) + BEGIN + --parent robot to tma public container + UPDATE dbo.entities SET parent=@publicContainer WHERE eid=@activeRobot + END + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/characterSettingsSetString.sql b/docs/db_structure/stored_procedures/characterSettingsSetString.sql new file mode 100644 index 0000000..0c8d55b --- /dev/null +++ b/docs/db_structure/stored_procedures/characterSettingsSetString.sql @@ -0,0 +1,32 @@ +/****** Object: StoredProcedure [dbo].[characterSettingsSetString] Script Date: 10.05.2026 13:27:42 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +create PROCEDURE [dbo].[characterSettingsSetString] + ( + @characterid int, + @data nvarchar(max) + ) + +AS + SET NOCOUNT ON + + if not exists ( select characterid from charactersettings where characterid=@characterid ) + begin + insert charactersettings (characterid, settingsstring) values (@characterid, @data) + end + else + begin + update charactersettings set settingsstring=@data where characterid=@characterid + end + + + + RETURN + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/checkGameConsistency.sql b/docs/db_structure/stored_procedures/checkGameConsistency.sql new file mode 100644 index 0000000..7e3720e --- /dev/null +++ b/docs/db_structure/stored_procedures/checkGameConsistency.sql @@ -0,0 +1,51 @@ +/****** Object: StoredProcedure [dbo].[checkGameConsistency] Script Date: 10.05.2026 13:29:51 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[checkGameConsistency] + +AS +BEGIN + + SET NOCOUNT ON; + + SELECT 0 FROM accounts; + + + /* + declare @disabledEntities int, @disabledMarketItems int, @disabledEnblerExtensions int, @usedInactiveExtensions int + declare @disabledComponents int + + set @disabledEntities = (select count(*) from entities where definition in (select definition from entitydefaults where enabled=0)) + select 'entities with disabled default found:' + cast(@disabledEntities as varchar(20)) + + set @disabledMarketItems = (select count(*) from marketitems where itemdefinition in (select definition from entitydefaults where enabled=0 or hidden=1)) + select 'marketitems with disabled default found: ' + cast(@disabledMarketItems as varchar(20)) + select 'definition list:' + select m.itemdefinition,d.definitionname as name from marketitems m join entitydefaults d on m.itemdefinition=d.definition where m.itemdefinition in (select definition from entitydefaults where enabled=0) + + set @disabledEnblerExtensions = (select count(*) from enablerextensions where extensionid in (select extensionid from extensions where active=0)) + select 'defaults with disabled enabler extension found: ' + cast(@disabledEnblerExtensions as varchar(20)) + select 'definition list:' + select e.definition,d.definitionname as name from enablerextensions e join entitydefaults d on e.definition =d.definition where e.extensionid in (select extensionid from extensions where active=0) + + set @usedInactiveExtensions = (select count(*) from characterextensions where extensionid in (select extensionid from extensions where active=0)) + select 'character extension found with inactive extension: ' + cast(@usedInactiveExtensions as varchar(20)) + select 'extension list:' + select c.extensionid,e.extensionname as name from characterextensions c join extensions e on c.extensionid=e.extensionid where c.extensionid in (select extensionid from extensions where active=0) + + set @disabledComponents = (select count(distinct definition) from components where componentdefinition in (select definition from entitydefaults where enabled=0) ) + select 'components with disabled definition found:' + cast(@disabledComponents as varchar(20)) + select 'definition with troubled component:' + select c.definition,d.definitionname as name from components c join entitydefaults d on c.definition=d.definition where c.componentdefinition in (select definition from entitydefaults where enabled=0) + */ +END + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/checkSyntaxInAllCode.sql b/docs/db_structure/stored_procedures/checkSyntaxInAllCode.sql new file mode 100644 index 0000000..1143164 --- /dev/null +++ b/docs/db_structure/stored_procedures/checkSyntaxInAllCode.sql @@ -0,0 +1,161 @@ +/****** Object: StoredProcedure [dbo].[checkSyntaxInAllCode] Script Date: 10.05.2026 13:30:34 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[checkSyntaxInAllCode] + +AS +BEGIN + +SET NOCOUNT ON; + +-- check for table existence +-- since i pimped it the first piece of code +-- seems to do the same as the second one, but who knows... + + +SELECT s.name, p.name, db = COALESCE(d.referenced_database_name, DB_NAME()), + [schema] = d.referenced_schema_name, [object] = d.referenced_entity_name + FROM sys.schemas AS s + INNER JOIN dbo.allCodingObjects() AS p + ON s.[schema_id] = p.[schema_id] + INNER JOIN sys.sql_expression_dependencies AS d + ON p.[object_id] = d.referencing_id + WHERE d.referenced_id IS NULL + AND d.referenced_server_name IS NULL + AND d.referenced_class = 1 + AND OBJECT_ID(QUOTENAME(COALESCE(d.referenced_database_name, DB_NAME())) + + '.' + QUOTENAME(d.referenced_schema_name) + + '.' + QUOTENAME(d.referenced_entity_name)) IS NULL + +--check for columns existence + + +--create temp table containing all sp, functions, and views we need to check + IF EXISTS (SELECT * FROM tempdb.dbo.sysobjects o WHERE o.xtype in ('U') and o.id = object_id(N'tempdb..#tempIdentityTables')) + DROP TABLE #tempIdentityTables + +CREATE TABLE #tempIdentityTables( + id int not null, + name [varchar](255) NOT NULL, + identOn int default (0), + ansiNullOn int default (0), + [owner] VARCHAR(255), + [objectType] VARCHAR(255), + [createdDate] datetime, + [definition] VARCHAR(MAX), --definition + command VARCHAR(MAX), --definition + results VARCHAR(MAX), + processed int default (0), + ) + +-- populate it with sp, views, and functions to be checked + insert into #tempIdentityTables (id, name, identOn, ansiNullOn, [owner], [objectType], [createdDate], [definition]) + select + o.id, + o.name, + OBJECTPROPERTY(id, 'ExecIsQuotedIdentOn') as quoted_ident_on, + OBJECTPROPERTY(id, 'ExecIsAnsiNullsOn') as ansi_nulls_on, + user_name(o.uid) owner , + --o.type, + pr.type_desc , + pr.create_date , + mod.definition + from sysobjects o + INNER JOIN sys.sql_modules mod ON o.id = mod.object_id + inner join + ( select object_id, name, type_desc,create_date from sys.views + union + select object_id, name, type_desc,create_date from sys.procedures + union + SELECT object_id, name, type_desc,create_date FROM sys.objects WHERE type IN ('FN', 'IF', 'TF','AF') -- scalar, inline table-valued, table-valued + ) as pr on pr.object_id = o.id + where category = 0 + +-- create the exec commands ready for the parse checks (unnessasary but good for debuggin, script intersection, code resuse) + DECLARE @id int + DECLARE @name VARCHAR(255) + DECLARE @owner VARCHAR(255) + DECLARE @identOn int + DECLARE @ansiNullOn int + DECLARE @SQL VARCHAR(MAX) + DECLARE @definition VARCHAR(MAX) + --set start condition cycle + SELECT TOP 1 @id = id, @name = name , @owner = [owner], @identOn = identOn, @ansiNullOn = ansiNullOn, @definition =[definition] FROM #tempIdentityTables WHERE processed = 0 + +WHILE @id IS NOT NULL + BEGIN + set @SQL = '' + +--if (@ansiNullOn > 0) + --SELECT @SQL = @SQL +'SET ANSI_NULLS ON ' + CHAR(13)+CHAR(10) + +--if (@identOn > 0) + --SELECT @SQL = @SQL +'SET QUOTED_IDENTIFIER ON ' + CHAR(13)+CHAR(10) + SELECT @SQL = @SQL +'SET FMTONLY ON ' + CHAR(13)+CHAR(10) + --SELECT @SQL = @SQL +'SET NOEXEC ON ' + CHAR(13)+CHAR(10) + +SELECT @SQL = @SQL + ' EXECUTE ('' '+ REPLACE(@definition,'''', '''''') +' '')' + CHAR(13)+CHAR(10) + +--SELECT @SQL = @SQL +'SET NOEXEC OFF ' + CHAR(13)+CHAR(10) + --SELECT @SQL = @SQL +'SET PARSEONLY OFF ' + CHAR(13)+CHAR(10) + +-- mark as processed + update #tempIdentityTables + set command = @SQL, processed = 1 + where #tempIdentityTables.id = @id + +--if no select the name would be last value and we'd get into an endless loop.... grrr + set @id = null; + SELECT TOP 1 @id = id, @name = name , @owner = [owner], @identOn = identOn, @ansiNullOn = ansiNullOn, @definition =[definition] FROM #tempIdentityTables WHERE processed = 0 + +END + +-- run actual queries + DECLARE @loopId int + DECLARE @ExecSQL NVARCHAR(MAX) + DECLARE @procName VARCHAR(255) = null + DECLARE @objectType VARCHAR(255) = null + +set @id = null + select top 1 @loopId = id, @ExecSQL = command, @procName = name, @objectType = objectType from #tempIdentityTables where processed = 0 OR processed = -1 OR processed = 1 + +WHILE @loopId IS NOT NULL + BEGIN + +BEGIN TRY + print 'Checking: ' + @procName + EXEC sp_executesql @ExecSQL + +update #tempIdentityTables + set processed = 2 + where #tempIdentityTables.id = @loopId + END TRY + BEGIN CATCH + +DECLARE @Failure varchar(MAX) = '' + select @Failure = ERROR_MESSAGE(); + print 'Error detected: ' + @Failure + update #tempIdentityTables + set processed = -2, + results = @Failure + where #tempIdentityTables.id = @loopId + END CATCH; + +--if no results then name would be last value and we'd get into an endless loop.... grrr + set @loopId = null; + select top 1 @loopId = id, @ExecSQL = command, @procName = name, @objectType = objectType from #tempIdentityTables where processed = 0 OR processed = -1 OR processed = 1 + +END + -- results returned are queries with errors + select results, * from #tempIdentityTables where results is not null + + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/cleanDisabledDefinitions.sql b/docs/db_structure/stored_procedures/cleanDisabledDefinitions.sql new file mode 100644 index 0000000..65f8a14 --- /dev/null +++ b/docs/db_structure/stored_procedures/cleanDisabledDefinitions.sql @@ -0,0 +1,22 @@ +/****** Object: StoredProcedure [dbo].[cleanDisabledDefinitions] Script Date: 10.05.2026 13:31:18 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[cleanDisabledDefinitions] + + + +AS +BEGIN + + + SET NOCOUNT ON; + + DELETE dbo.entities WHERE definition in (SELECT definition FROM dbo.entitydefaults WHERE enabled=0) + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/cleanUpGame.sql b/docs/db_structure/stored_procedures/cleanUpGame.sql new file mode 100644 index 0000000..3897a57 --- /dev/null +++ b/docs/db_structure/stored_procedures/cleanUpGame.sql @@ -0,0 +1,67 @@ +/****** Object: StoredProcedure [dbo].[cleanUpGame] Script Date: 10.05.2026 13:32:02 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + + + +CREATE PROCEDURE [dbo].[cleanUpGame] + +AS +BEGIN tran + + --inactive characters lifespan < inactivespan + select characterID from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate()))) + + --market + + --disabled items on market + --LIST + select mi.itemdefinition,d.definitionname from marketitems mi join entitydefaults d on mi.itemdefinition=d.definition where mi.itemdefinition in (select definition from entitydefaults where enabled=0) + --DELETE + delete marketitems where itemdefinition in (select definition from entitydefaults where enabled=0) + + --items belong to inactive characters + --LIST + select mi.itemdefinition,d.definitionname,c.nick from marketitems mi join entitydefaults d on mi.itemdefinition=d.definition join characters c on mi.submittereid=c.rooteid + where mi.submittereid in (select rooteid from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate())))) + --DELETE + delete marketitems where submittereid in (select rooteid from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate())))) + + + + --character extension table + + --inactive extensions + --LIST + select e.extensionname,ce.extensionid from characterextensions ce join extensions e on e.extensionid=ce.extensionid where ce.extensionid in (select extensionid from extensions where active=0) + --DELETE + delete characterextensions where extensionid in (select extensionid from extensions where active=0) + + --extensions for inactive characters + --LIST + select e.characterextensionid,c.nick as nick from characterextensions e join characters c on e.characterid=c.characterid where e.characterid in + (select characterID from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate())))) + --DELETE + delete characterextensions where characterid in (select characterID from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate())))) + + --character settings + --LIST + select s.characterid, c.nick as nick from charactersettings s join characters c on s.characterid=c.characterid where s.characterid in (select characterID from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate())))) + --DELETE + delete charactersettings where characterid in (select characterID from characters where active=0 and ((datediff(minute, creation,deletedat)) < (datediff(minute, deletedat, getdate())))) + + + --delete entities where definition in (select definition from entitydefaults where enabled=0) + --select 'entities deleted: ' + cast(@@ROWCOUNT as varchar) +rollback + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/cleanUpInsuranceByBaseEid.sql b/docs/db_structure/stored_procedures/cleanUpInsuranceByBaseEid.sql new file mode 100644 index 0000000..e506c98 --- /dev/null +++ b/docs/db_structure/stored_procedures/cleanUpInsuranceByBaseEid.sql @@ -0,0 +1,23 @@ +/****** Object: StoredProcedure [dbo].[cleanUpInsuranceByBaseEid] Script Date: 10.05.2026 13:32:49 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[cleanUpInsuranceByBaseEid] + + @baseEid bigint + +AS +BEGIN + + + SET NOCOUNT ON; + DELETE dbo.insurance WHERE eid IN (SELECT eid FROM dbo.treeEids(@baseEid)) + SELECT @@ROWCOUNT + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/cleanUpMarket.sql b/docs/db_structure/stored_procedures/cleanUpMarket.sql new file mode 100644 index 0000000..9e2c216 --- /dev/null +++ b/docs/db_structure/stored_procedures/cleanUpMarket.sql @@ -0,0 +1,19 @@ +/****** Object: StoredProcedure [dbo].[cleanUpMarket] Script Date: 10.05.2026 13:33:25 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[cleanUpMarket] + +AS +BEGIN + SET NOCOUNT ON; + + delete marketitems where itemdefinition in (select definition from entitydefaults where enabled=0 or hidden=1) + + select @@ROWCOUNT + END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/cleanUpZoneUserEntities.sql b/docs/db_structure/stored_procedures/cleanUpZoneUserEntities.sql new file mode 100644 index 0000000..39a915d --- /dev/null +++ b/docs/db_structure/stored_procedures/cleanUpZoneUserEntities.sql @@ -0,0 +1,19 @@ +/****** Object: StoredProcedure [dbo].[cleanUpZoneUserEntities] Script Date: 10.05.2026 13:34:34 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[cleanUpZoneUserEntities] + +AS +BEGIN + + SET NOCOUNT ON; + + delete dbo.zoneuserentities WHERE eid NOT IN (SELECT eid FROM dbo.entities) + SELECT @@ROWCOUNT +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/consolidate_statistics.sql b/docs/db_structure/stored_procedures/consolidate_statistics.sql new file mode 100644 index 0000000..7a5dcc2 --- /dev/null +++ b/docs/db_structure/stored_procedures/consolidate_statistics.sql @@ -0,0 +1,73 @@ +/****** Object: StoredProcedure [dbo].[consolidate_statistics] Script Date: 10.05.2026 13:35:20 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[consolidate_statistics] +AS +BEGIN + -- SET NOCOUNT ON added to prevent extra result sets from + -- interfering with SELECT statements. + SET NOCOUNT ON; + + -- Insert statements for procedure here + -- This block consolidates and cleans up daily statistics for resources + + -- Step 1: Aggregate from Table B (buffer) + WITH Aggregated AS ( + SELECT + gathered_on, + resource_name, + SUM(quantity) AS total_quantity + FROM resources_gathered_daily WITH (READPAST) -- Skip locked rows (optional) + GROUP BY gathered_on, resource_name + ) + + -- Step 2: Merge into Table A (summary) + MERGE INTO resources_gathered AS target + USING Aggregated AS source + ON target.gathered_on = source.gathered_on + AND target.resource_name = source.resource_name + WHEN MATCHED THEN + UPDATE SET quantity = target.quantity + source.total_quantity + WHEN NOT MATCHED THEN + INSERT (gathered_on, resource_name, quantity) + VALUES (source.gathered_on, source.resource_name, source.total_quantity); + + -- Step 3: Delete processed rows from Table B + DELETE FROM resources_gathered_daily; + + -- end of block + + -- This block consolidates and cleans up daily statistics for plasma + + -- Step 1: Aggregate from Table B (buffer) + WITH Aggregated AS ( + SELECT + gathered_on, + plasma_type, + SUM(quantity) AS total_quantity + FROM plasma_gathered_daily WITH (READPAST) -- Skip locked rows (optional) + GROUP BY gathered_on, plasma_type + ) + + -- Step 2: Merge into Table A (summary) + MERGE INTO plasma_gathered AS target + USING Aggregated AS source + ON target.gathered_on = source.gathered_on + AND target.plasma_type = source.plasma_type + WHEN MATCHED THEN + UPDATE SET quantity = target.quantity + source.total_quantity + WHEN NOT MATCHED THEN + INSERT (gathered_on, plasma_type, quantity) + VALUES (source.gathered_on, source.plasma_type, source.total_quantity); + + -- Step 3: Delete processed rows from Table B + DELETE FROM plasma_gathered_daily; + + -- end of block +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/copyArtifactLoot.sql b/docs/db_structure/stored_procedures/copyArtifactLoot.sql new file mode 100644 index 0000000..b49eb27 --- /dev/null +++ b/docs/db_structure/stored_procedures/copyArtifactLoot.sql @@ -0,0 +1,31 @@ +/****** Object: StoredProcedure [dbo].[copyArtifactLoot] Script Date: 10.05.2026 13:35:56 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +-- would be nice to make it table independent in the future... + +CREATE PROCEDURE [dbo].[copyArtifactLoot] + + @from int = 0, + @to int = 0 +AS +BEGIN + + SET NOCOUNT ON; + + INSERT dbo.artifactloot ( + artifacttype, + definition, + minquantity, + maxquantity, + chance +) + +SELECT @to,definition,minquantity,maxquantity,chance FROM artifactloot WHERE artifacttype = @from + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/copyComponents.sql b/docs/db_structure/stored_procedures/copyComponents.sql new file mode 100644 index 0000000..f05064a --- /dev/null +++ b/docs/db_structure/stored_procedures/copyComponents.sql @@ -0,0 +1,38 @@ +/****** Object: StoredProcedure [dbo].[copyComponents] Script Date: 10.05.2026 13:37:06 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[copyComponents] + + @sourceDefinition int, + @targetDefinition int +AS + + begin tran + + if not exists (select * from entitydefaults where definition=@sourceDefinition) + begin + rollback + return + end + + if not exists (select * from entitydefaults where definition=@targetDefinition) + begin + rollback + return + end + + delete components where definition=@targetDefinition + insert components select @targetDefinition,componentdefinition,componentamount from components where definition=@sourceDefinition + --delete components where definition=@targetDefinition and componentamount=1 --delete the license + + select * from components where definition=@targetDefinition + commit + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/copyLVL1ArtifactLoot.sql b/docs/db_structure/stored_procedures/copyLVL1ArtifactLoot.sql new file mode 100644 index 0000000..93697a0 --- /dev/null +++ b/docs/db_structure/stored_procedures/copyLVL1ArtifactLoot.sql @@ -0,0 +1,33 @@ +/****** Object: StoredProcedure [dbo].[copyLVL1ArtifactLoot] Script Date: 10.05.2026 13:37:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[copyLVL1ArtifactLoot] + + @from int , + @to int +AS +BEGIN + SET NOCOUNT ON; + + +--LEVEL 1 +INSERT dbo.artifactloot ( + artifacttype, + definition, + minquantity, + maxquantity, + chance +) + +SELECT @to,al.definition,al.minquantity,al.maxquantity, CASE WHEN ed.definitionname LIKE '%standard%' THEN 0.015 ELSE 0.05 end +FROM artifactloot al JOIN entitydefaults ed ON al.definition=ed.definition +WHERE al.artifacttype=@from +AND (ed.definitionname NOT LIKE '%artifact_a%') + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/copyLVL3ArtifactLoot.sql b/docs/db_structure/stored_procedures/copyLVL3ArtifactLoot.sql new file mode 100644 index 0000000..fff7024 --- /dev/null +++ b/docs/db_structure/stored_procedures/copyLVL3ArtifactLoot.sql @@ -0,0 +1,35 @@ +/****** Object: StoredProcedure [dbo].[copyLVL3ArtifactLoot] Script Date: 10.05.2026 13:38:23 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[copyLVL3ArtifactLoot] + + @from int , + @to int +AS +BEGIN + SET NOCOUNT ON; + + +--LEVEL 3 + +INSERT dbo.artifactloot ( + artifacttype, + definition, + minquantity, + maxquantity, + chance +) + + +SELECT @to,al.definition,al.minquantity,al.maxquantity, CASE WHEN ed.definitionname LIKE '%standard%' THEN 0.05 ELSE 0.015 end +FROM artifactloot al JOIN entitydefaults ed ON al.definition=ed.definition +WHERE al.artifacttype=@from +AND (ed.definitionname NOT LIKE '%artifact_damaged%') + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/copycharacterwizard.sql b/docs/db_structure/stored_procedures/copycharacterwizard.sql new file mode 100644 index 0000000..d5fa056 --- /dev/null +++ b/docs/db_structure/stored_procedures/copycharacterwizard.sql @@ -0,0 +1,129 @@ +/****** Object: StoredProcedure [dbo].[copycharacterwizard] Script Date: 10.05.2026 13:36:28 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[copycharacterwizard] + +AS +BEGIN + + + SET NOCOUNT ON; + + + DELETE dbo.cw_major_extension WHERE majorid>9 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 10,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=1 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 19,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=1 + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 11,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=2 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 20,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=2 + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 12,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=3 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 21,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=3 + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 13,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=4 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 22,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=4 + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 14,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=5 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 23,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=5 + + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 15,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=6 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 24,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=6 + + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 16,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=7 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 25,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=7 + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 17,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=8 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 26,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=8 + + + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 18,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=9 + +INSERT dbo.cw_major_extension ( majorid,extensionid,levelincrement) +SELECT 27,extensionid,levelincrement FROM dbo.cw_major_extension WHERE majorid=9 + + + + + +DELETE dbo.cw_school_extension WHERE schoolid>3 + +INSERT dbo.cw_school_extension (schoolid,extensionid,levelincrement) +SELECT 4, extensionid,levelincrement FROM dbo.cw_school_extension WHERE schoolid=1 + +INSERT dbo.cw_school_extension (schoolid,extensionid,levelincrement) +SELECT 7, extensionid,levelincrement FROM dbo.cw_school_extension WHERE schoolid=1 + + +INSERT dbo.cw_school_extension (schoolid,extensionid,levelincrement) +SELECT 5, extensionid,levelincrement FROM dbo.cw_school_extension WHERE schoolid=2 + +INSERT dbo.cw_school_extension (schoolid,extensionid,levelincrement) +SELECT 8, extensionid,levelincrement FROM dbo.cw_school_extension WHERE schoolid=2 + + +INSERT dbo.cw_school_extension (schoolid,extensionid,levelincrement) +SELECT 6, extensionid,levelincrement FROM dbo.cw_school_extension WHERE schoolid=3 + +INSERT dbo.cw_school_extension (schoolid,extensionid,levelincrement) +SELECT 9, extensionid,levelincrement FROM dbo.cw_school_extension WHERE schoolid=3 + + + + + + + + + + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/countParents.sql b/docs/db_structure/stored_procedures/countParents.sql new file mode 100644 index 0000000..d6158fd --- /dev/null +++ b/docs/db_structure/stored_procedures/countParents.sql @@ -0,0 +1,36 @@ +/****** Object: StoredProcedure [dbo].[countParents] Script Date: 10.05.2026 13:39:00 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +--counts the parenting depth for the input eid + +CREATE PROCEDURE [dbo].[countParents] + + @source bigint + +AS +BEGIN + + SET NOCOUNT ON; + declare @counter int, @tmpParent bigint + + set @counter =0 + set @tmpParent = (select parent from entities where eid=@source) + + while (@tmpParent is not null) + begin + + set @tmpParent = (select parent from entities where eid=@tmpParent) + set @counter = @counter + 1 + + end + + SELECT @counter +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/createVendor.sql b/docs/db_structure/stored_procedures/createVendor.sql new file mode 100644 index 0000000..22f58f0 --- /dev/null +++ b/docs/db_structure/stored_procedures/createVendor.sql @@ -0,0 +1,58 @@ +/****** Object: StoredProcedure [dbo].[createVendor] Script Date: 10.05.2026 13:41:35 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[createVendor] +( + @definition int, + @marketEID bigint, + @sellProfit float, + @buyProfit float +) +AS + +BEGIN tran + +declare @owner as bigint + +select @owner = owner from entities where eid = @marketEID + +declare @vendorEID as bigint + +declare @baseEID as bigint + +select @baseEID = parent from entities where eid = @marketEID + +insert into entities (definition, owner, parent) values (@definition,@owner,@baseEID) + +select @vendorEID = eid from entities where eid = scope_identity() + +if @vendorEID = 0 +begin + select null as vendorEID + rollback + return +end + +insert vendors (vendoreid,marketeid,vendorsellprofit,vendorbuyprofit) values (@vendorEID,@marketEID,@sellProfit,@buyProfit) + +select @vendorEID as vendorEID + +commit + + + + + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_crm.sql b/docs/db_structure/stored_procedures/debug_crm.sql new file mode 100644 index 0000000..aa97aaf --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_crm.sql @@ -0,0 +1,116 @@ +/****** Object: StoredProcedure [dbo].[debug_crm] Script Date: 10.05.2026 13:44:32 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_crm] + +AS +BEGIN + SET NOCOUNT ON; + +exec dbo.debug_disablenpcs +exec dbo.debug_disablezones +--EXEC dbo.debug_enablenpcs + +--new virginia + +--UPDATE dbo.zones SET [enabled]=1 WHERE protected=1 + +update zones set enabled=1 WHERE [name]='zone_TM' + + +/* +--attalica +update zones set enabled=1 where [name]='zone_ICS' +*/ + +/* +--daoden +update zones set enabled=1 where id=2 +*/ + +/* +--tellesis +UPDATE dbo.zones SET enabled=1 WHERE id=6 +*/ + +--shinjalar +/* +update zones set enabled=1 where [name]='zone_ASI_pve' +update plugins set enabled=1 where pluginname='zone_7' +*/ + +/* +--hershfield +update zones set enabled=1 where [name]='zone_TM_pve' +*/ + + +--update zones set enabled=1 where [name]='zone_terraform_test' + + +/* +update zones set enabled=1 where [name]='zoneTestBeta' +*/ + +/* +--UPDATE npcpresence SET enabled=1 WHERE id=1562 +*/ + +/* +update zones set enabled=0 where [name]='zone_training' +*/ + + +/* +update zones set enabled=1 where [name]='zone_ASI_pvp' +*/ + +/* +update zones set enabled=1 where [name]='zone_ICS_A_real' +*/ + +/* +update zones set enabled=1 where [name]='zone_tm_g_6' +*/ + +/* +update zones set enabled=1 where [name]='zone_tm_g_7' +*/ + +/* +UPDATE zones SET enabled=1 WHERE [name]='zone_tm_g_1' + +UPDATE zones SET enabled=1 WHERE [name]='zone_ics_g_1' + +UPDATE zones SET enabled=1 WHERE [name]='zone_ics_g_2' +*/ + +/* +UPDATE zones SET enabled=1 WHERE [name]='zone_ICS_A_real' +*/ + +--debug npc presence + +--UPDATE dbo.npcpresence SET enabled=1 WHERE id=1044 + +--update npcpresence set enabled=0 where [name]='debug_presence' +--update npcpresence set enabled=0 where id=1172 + +--UPDATE npcpresence SET enabled=0 WHERE [name]='random_flock_gatherer' + +/* +UPDATE dbo.zones SET enabled = 1 where id >=20 +*/ + +END + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_devserver.sql b/docs/db_structure/stored_procedures/debug_devserver.sql new file mode 100644 index 0000000..f5a3d4a --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_devserver.sql @@ -0,0 +1,54 @@ +/****** Object: StoredProcedure [dbo].[debug_devserver] Script Date: 10.05.2026 13:45:11 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_devserver] + +AS +BEGIN + SET NOCOUNT ON; + +exec dbo.debug_disablezones + +exec dbo.debug_disablenpcs +exec dbo.debug_enablenpcs +exec dbo.debug_enableallzones + +update zones set enabled=1 where [name]='zone_TM' + + + +UPDATE zones SET enabled=1 WHERE [name]='zone_terraform_test' + +UPDATE zones SET enabled=0 WHERE [name]='zone_pvp_arena' + + +update zones set enabled=0 where [name]='zone_mini' + + +update zones set enabled=1 where [name]='zone_training' + + + +update zones set enabled=1 where [name]='zoneTestBeta' + +UPDATE npcpresence SET enabled=0 WHERE id=1562 + + +UPDATE dbo.zones SET enabled=1 WHERE id IN (22, 25, 30, 35, 36, 39 ) + + + +--disable debug presences +update npcpresence set enabled=0 where [name] in ( 'random_flock_gatherer','debug_presence') + + +--UPDATE dbo.npcpresence SET enabled=1 WHERE presencetype=1 + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_disablenpcs.sql b/docs/db_structure/stored_procedures/debug_disablenpcs.sql new file mode 100644 index 0000000..aaf035e --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_disablenpcs.sql @@ -0,0 +1,20 @@ +/****** Object: StoredProcedure [dbo].[debug_disablenpcs] Script Date: 10.05.2026 13:45:46 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_disablenpcs] + +AS +BEGIN + SET NOCOUNT ON; + + +UPDATE npcpresence SET enabled = 0 --ALL off +UPDATE npcpresence SET enabled = 1 WHERE presencetype=2 OR presencetype=4 --dynamic on, dynpool on + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_disablezones.sql b/docs/db_structure/stored_procedures/debug_disablezones.sql new file mode 100644 index 0000000..55e463f --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_disablezones.sql @@ -0,0 +1,16 @@ +/****** Object: StoredProcedure [dbo].[debug_disablezones] Script Date: 10.05.2026 13:46:21 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_disablezones] + +AS +BEGIN + SET NOCOUNT ON; +UPDATE dbo.zones SET enabled = 0 +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_enableallzones.sql b/docs/db_structure/stored_procedures/debug_enableallzones.sql new file mode 100644 index 0000000..158a775 --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_enableallzones.sql @@ -0,0 +1,31 @@ +/****** Object: StoredProcedure [dbo].[debug_enableallzones] Script Date: 10.05.2026 13:47:06 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_enableallzones] + +AS +BEGIN + SET NOCOUNT ON; + + + +UPDATE dbo.zones SET enabled = 1 where [name] LIKE 'zone_%' + +--pvp arena + quadmap off + +UPDATE dbo.zones SET enabled = 0 where id IN (16,12,13,14,15) + +--masik jacco off + +UPDATE dbo.zones SET enabled=0 WHERE id=46 + +UPDATE dbo.zones SET enabled = 0 where id >=20 AND id <= 44 + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_enablenpcs.sql b/docs/db_structure/stored_procedures/debug_enablenpcs.sql new file mode 100644 index 0000000..edc080b --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_enablenpcs.sql @@ -0,0 +1,21 @@ +/****** Object: StoredProcedure [dbo].[debug_enablenpcs] Script Date: 10.05.2026 13:47:45 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_enablenpcs] + +AS +BEGIN + SET NOCOUNT ON; + + +UPDATE npcpresence SET enabled = 1 +UPDATE npcpresence SET enabled=0 WHERE [name]='random_flock_gatherer' +update npcpresence set enabled=0 where [name]='debug_presence' + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_junior.sql b/docs/db_structure/stored_procedures/debug_junior.sql new file mode 100644 index 0000000..938d719 --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_junior.sql @@ -0,0 +1,34 @@ +/****** Object: StoredProcedure [dbo].[debug_junior] Script Date: 10.05.2026 13:58:06 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[debug_junior] + +AS +BEGIN + SET NOCOUNT ON; + +--exec dbo.debug_enablenpcs +exec dbo.debug_disablenpcs +exec dbo.debug_disablezones + + +update zones set enabled=1 where [name]='zoneTestBeta' + +update zones set enabled=1 where [name]='zone_terraform_test' + + + + +--debug npc presence +update npcpresence set enabled=1 where name='debug_presence' +--update npcpresence set enabled=1 where name='debug_random' +update npcpresence set enabled=1 where name='junior_roaming_teszt' + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/debug_liveserver.sql b/docs/db_structure/stored_procedures/debug_liveserver.sql new file mode 100644 index 0000000..0bde48a --- /dev/null +++ b/docs/db_structure/stored_procedures/debug_liveserver.sql @@ -0,0 +1,59 @@ +/****** Object: StoredProcedure [dbo].[debug_liveserver] Script Date: 10.05.2026 13:59:22 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[debug_liveserver] + +AS +BEGIN + SET NOCOUNT ON; + +exec dbo.debug_disablezones + +exec dbo.debug_enablenpcs +exec dbo.debug_enableallzones + +update zones set enabled=1 where [name]='zone_TM' + + + +UPDATE zones SET enabled=0 WHERE [name]='zone_terraform_test' + +UPDATE zones SET enabled=0 WHERE [name]='zone_pvp_arena' + + + + +UPDATE zones SET enabled=0 WHERE [name]='zone_mini' + + +--disable debug presences +update npcpresence set enabled=0 where [name] in ( 'random_flock_gatherer','debug_presence') + + +--gamma off +UPDATE dbo.zones SET enabled = 0 where id >=20 AND id <= 43 + + + + + + +update zones set enabled=1 where [name]='zone_training' + + + +UPDATE dbo.zones SET enabled=1 WHERE id IN (22, 25, 30, 35, 36, 39 ) + + +UPDATE zones SET enabled=0 WHERE [name]='zone_gammalab' + + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/deleteAllChildren.sql b/docs/db_structure/stored_procedures/deleteAllChildren.sql new file mode 100644 index 0000000..c9e069f --- /dev/null +++ b/docs/db_structure/stored_procedures/deleteAllChildren.sql @@ -0,0 +1,33 @@ +/****** Object: StoredProcedure [dbo].[deleteAllChildren] Script Date: 10.05.2026 14:00:26 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[deleteAllChildren] +( + @rootEID bigint -- the node which will be used as root +) + + +AS + SET NOCOUNT ON; + +with children(eid,parent,lvl) + as + ( + SELECT @rootEid,@rootEid,0 + + UNION ALL + + SELECT C.eid,C.parent, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid + ) + + delete entities where eid in ( select eid from children ) AND eid<>@rootEID option (MAXRECURSION 256) + +SELECT @@ROWCOUNT + +RETURN +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/deleteTree.sql b/docs/db_structure/stored_procedures/deleteTree.sql new file mode 100644 index 0000000..f8897d4 --- /dev/null +++ b/docs/db_structure/stored_procedures/deleteTree.sql @@ -0,0 +1,41 @@ +/****** Object: StoredProcedure [dbo].[deleteTree] Script Date: 10.05.2026 15:04:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[deleteTree] +( + @rootEID bigint -- the node which will be used as root +) + + +--!!!!!! ADMIN TOOL !!!!!! + +AS + SET NOCOUNT ON; + +with children(eid,parent,lvl) + as + ( + SELECT eid,parent,0 FROM entities WHERE eid = @rootEID + + UNION ALL + + SELECT C.eid,C.parent, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid + ) + + delete entities where eid in ( select eid from children ) option (MAXRECURSION 256) + + +RETURN + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/deleteUnusedPublicChannels.sql b/docs/db_structure/stored_procedures/deleteUnusedPublicChannels.sql new file mode 100644 index 0000000..14b3d2f --- /dev/null +++ b/docs/db_structure/stored_procedures/deleteUnusedPublicChannels.sql @@ -0,0 +1,51 @@ +/****** Object: StoredProcedure [dbo].[deleteUnusedPublicChannels] Script Date: 10.05.2026 15:05:18 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[deleteUnusedPublicChannels] + + + +AS +BEGIN + SET NOCOUNT ON; + + +SELECT COUNT(*),'channels before' FROM dbo.channels WHERE [type]=0; + +DECLARE @threshold DATETIME; +SET @threshold = DATEADD(MONTH, -3, GETDATE()); +SELECT @threshold; + +DECLARE @minLogin DATETIME; +DECLARE @channelId INT; +DECLARE channels CURSOR STATIC LOCAL READ_ONLY FORWARD_ONLY FOR +SELECT id FROM dbo.channels WHERE [type]=0; -- public channels +OPEN channels; +FETCH NEXT FROM channels INTO @channelId; +WHILE (@@FETCH_STATUS = 0) +BEGIN + + SELECT @minLogin = MAX(lastUsed) FROM dbo.characters WHERE characterID IN (SELECT memberid FROM dbo.channelmembers WHERE channelid=@channelId) + + IF (@minLogin IS NULL or @minLogin < @threshold) + BEGIN + SELECT * FROM channels WHERE id=@channelId + DELETE dbo.channelmembers WHERE channelid=@channelId; + DELETE dbo.channels WHERE id=@channelId; + END + + + FETCH NEXT FROM channels INTO @channelId; +END +CLOSE channels; DEALLOCATE channels; +SELECT COUNT(*),'channels after' FROM dbo.channels WHERE [type]=0; + + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/dockInCharacterAndRobot.sql b/docs/db_structure/stored_procedures/dockInCharacterAndRobot.sql new file mode 100644 index 0000000..6d544be --- /dev/null +++ b/docs/db_structure/stored_procedures/dockInCharacterAndRobot.sql @@ -0,0 +1,49 @@ +/****** Object: StoredProcedure [dbo].[dockInCharacterAndRobot] Script Date: 10.05.2026 15:05:57 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[dockInCharacterAndRobot] + @characterId INT +AS +BEGIN + SET NOCOUNT ON; + + -- dock the character in +DECLARE @baseEid BIGINT, @publicContainer BIGINT, @robotEid BIGINT; +SELECT @baseEid=baseEID, @robotEid=activeChassis FROM characters WHERE characterID=@characterId; + +-- check base +IF (dbo.isEntityExists(@baseEid)=0) +BEGIN + SELECT 'ERROR: base not valid', @baseEid AS baseEid, @characterId AS characterId; + RETURN +END + +-- write character +UPDATE characters SET docked=1,zoneID=NULL, positionX=NULL, positionY=NULL WHERE characterID=@characterId; +SELECT 'OK: character got docked in.', @baseEid AS baseEid, @characterId AS characterId; + +-- process robot +IF (@robotEid IS NULL OR dbo.isEntityExists(@robotEid)=0) +BEGIN + SELECT 'OK: robot eid NULL, or not valid, process finished.', @characterId AS characterId, @robotEid AS robotEid; + RETURN +END + +-- check public container +SET @publicContainer = dbo.GetPublicContainerEidByBaseEid(@baseEid); +IF (@publicContainer IS NULL or dbo.isEntityExists(@publicContainer)=0) +BEGIN + SELECT 'ERROR: public container not valid.', @publicContainer AS publicContainer; + RETURN +END + +UPDATE dbo.entities SET parent=@publicContainer WHERE eid=@robotEid; +SELECT 'OK: robot got parented to public container.', @publicContainer AS publicContainer, @baseEid AS baseEid, @robotEid AS robotEid, @characterId AS characterId; + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/entitiesFixParenting.sql b/docs/db_structure/stored_procedures/entitiesFixParenting.sql new file mode 100644 index 0000000..3fa8265 --- /dev/null +++ b/docs/db_structure/stored_procedures/entitiesFixParenting.sql @@ -0,0 +1,32 @@ +/****** Object: StoredProcedure [dbo].[entitiesFixParenting] Script Date: 10.05.2026 15:08:25 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[entitiesFixParenting] + + +AS +BEGIN + +DECLARE @isRun INT +SET @isRun = 1; + +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_robots', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_robot_components', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_robot_equipment', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_robot_inventory', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_container', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_ammo', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_material', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_station_services', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_production_items', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_field_accessories', @run=@isRun +EXEC dbo.entitiesReportAndDeleteOrphanedByCf @cfString = 'cf_documents', @run=@isRun + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/entitiesReportAndDeleteOrphanedByCf.sql b/docs/db_structure/stored_procedures/entitiesReportAndDeleteOrphanedByCf.sql new file mode 100644 index 0000000..1f93868 --- /dev/null +++ b/docs/db_structure/stored_procedures/entitiesReportAndDeleteOrphanedByCf.sql @@ -0,0 +1,40 @@ +/****** Object: StoredProcedure [dbo].[entitiesReportAndDeleteOrphanedByCf] Script Date: 10.05.2026 15:09:14 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[entitiesReportAndDeleteOrphanedByCf] + @cfString VARCHAR(512), + @run INT = 0 +AS +BEGIN + + +----check by cf string +SELECT COUNT(*),@cfString FROM dbo.entities WHERE definition in +(SELECT definition FROM dbo.getDefinitionByCFString(@cfString)) +AND +parent IS NOT NULL AND parent NOT IN (SELECT eid FROM dbo.entities) + + +IF (@run =0) +BEGIN + RETURN +END + +--delete by cf +DELETE dbo.entities WHERE definition IN +(SELECT definition FROM dbo.getDefinitionByCFString(@cfString)) +AND +parent IS NOT NULL AND parent NOT IN (SELECT eid FROM dbo.entities) + + + + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/epForActivityLogList.sql b/docs/db_structure/stored_procedures/epForActivityLogList.sql new file mode 100644 index 0000000..5b48d5b --- /dev/null +++ b/docs/db_structure/stored_procedures/epForActivityLogList.sql @@ -0,0 +1,28 @@ +/****** Object: StoredProcedure [dbo].[epForActivityLogList] Script Date: 10.05.2026 15:11:18 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[epForActivityLogList] + + @accountId int, + @earlier DATETIME, + @later DATETIME +AS +BEGIN + SET NOCOUNT ON; + + + +SELECT k.characterid, k.yearpart,k.monthpart,k.daypart, k.epforactivitytype, SUM(points) AS [points] from +( +SELECT *,DATEPART(YEAR,eventtime) AS [yearpart],DATEPART(MONTH, eventtime) AS [monthpart],DATEPART(DAY,eventtime) AS [daypart] FROM dbo.epforactivitylog WHERE accountid=@accountId AND (eventtime BETWEEN @earlier AND @later) +) k +GROUP BY k.yearpart, k.monthpart, k.daypart, k.epforactivitytype,k.characterid +ORDER BY k.yearpart, k.monthpart, k.daypart, k.epforactivitytype asc + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionPointsAdd.sql b/docs/db_structure/stored_procedures/extensionPointsAdd.sql new file mode 100644 index 0000000..965330c --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionPointsAdd.sql @@ -0,0 +1,37 @@ +/****** Object: StoredProcedure [dbo].[extensionPointsAdd] Script Date: 10.05.2026 15:20:25 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionPointsAdd] + + @basePoints INT, --normal account + @bonusPoints INT --paid account + +AS +BEGIN + SET NOCOUNT ON; + + +DECLARE @nofAffectedAccounts INT , @paying INT, @now DATETIME + +SET @now = GETDATE() --real db server time + +INSERT dbo.extensionpoints( accountid, points ) + SELECT accountid,@basePoints FROM accounts + +--collect data for log +SET @nofAffectedAccounts = @@ROWCOUNT + +--write log +INSERT dbo.extensionpointworklog ( total,paying ) VALUES ( @nofAffectedAccounts, @nofAffectedAccounts ) + +EXEC dbo.artifactReset; -- needs to be moved from here !!! yes, daily task... but + +SELECT @now + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionPointsCheck.sql b/docs/db_structure/stored_procedures/extensionPointsCheck.sql new file mode 100644 index 0000000..0d21811 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionPointsCheck.sql @@ -0,0 +1,30 @@ +/****** Object: StoredProcedure [dbo].[extensionPointsCheck] Script Date: 10.05.2026 15:22:04 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionPointsCheck] + + @now DATETIME + +AS +BEGIN + SET NOCOUNT ON; + + +DECLARE @year INT, @month INT, @day INT + + SET @year = DATEPART(yy, @now ) + SET @month = DATEPART(mm, @now) + SET @day = DATEPART(dd, @now) + +SELECT COUNT(*) FROM dbo.extensionpointworklog WHERE +DATEPART(yy, eventtime) = @year AND +DATEPART(mm, eventtime) = @month AND +DATEPART(dd, eventtime) = @day + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionPointsConsolidate.sql b/docs/db_structure/stored_procedures/extensionPointsConsolidate.sql new file mode 100644 index 0000000..1a0e829 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionPointsConsolidate.sql @@ -0,0 +1,41 @@ +/****** Object: StoredProcedure [dbo].[extensionPointsConsolidate] Script Date: 10.05.2026 15:22:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionPointsConsolidate] +AS +BEGIN + +--colidates the extension points in a transaction + +SET NOCOUNT ON + +DECLARE @now DATETIME +SET @now = GETDATE() +--WAITFOR DELAY '00:00:01'; + +-- this will take time anyway +CREATE NONCLUSTERED INDEX [IX_h1] +ON [dbo].[extensionpoints] ([eventtime]) +INCLUDE ([accountid]) + +SET TRANSACTION ISOLATION LEVEL READ COMMITTED +BEGIN TRANSACTION + + --insert sum + INSERT extensionpoints (accountid,points) + SELECT accountid,SUM(points) FROM dbo.extensionpoints GROUP BY accountid + + --delete old records + DELETE dbo.extensionpoints WHERE eventtime < @now + +COMMIT + +DROP INDEX [IX_h1] ON [dbo].[extensionpoints] + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionPointsInject.sql b/docs/db_structure/stored_procedures/extensionPointsInject.sql new file mode 100644 index 0000000..4e86766 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionPointsInject.sql @@ -0,0 +1,22 @@ +/****** Object: StoredProcedure [dbo].[extensionPointsInject] Script Date: 10.05.2026 15:24:03 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionPointsInject] + + @accountId int , + @points int + AS +BEGIN + + SET NOCOUNT ON; + + insert extensionpoints (accountid,points) values (@accountId,@points) + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionRevert.sql b/docs/db_structure/stored_procedures/extensionRevert.sql new file mode 100644 index 0000000..1c193a2 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionRevert.sql @@ -0,0 +1,44 @@ +/****** Object: StoredProcedure [dbo].[extensionRevert] Script Date: 10.05.2026 15:25:41 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionRevert] + @extensionID INT, + @fee INT +AS +BEGIN + + +SET NOCOUNT ON +DECLARE @characterID INT, @count int + +DECLARE characterz CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR + SELECT distinct characterid FROM accountextensionspent WHERE extensionid=@extensionID + +OPEN characterz +FETCH NEXT FROM characterz INTO @characterID + +WHILE @@FETCH_STATUS = 0 + BEGIN + + update characters set credit=credit+@fee where characterid=@characterID; + DELETE characterextensions WHERE extensionid=@extensionID; + DELETE accountextensionspent WHERE extensionid=@extensionID; + + SET @count = @count +1 + + FETCH NEXT FROM characterz INTO @characterID + END + +CLOSE characterz +DEALLOCATE characterz + +SELECT @characterID AS [nofCharacters], 'characters got processed' AS [message], @extensionID AS [extensionId] + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionRevertV2.sql b/docs/db_structure/stored_procedures/extensionRevertV2.sql new file mode 100644 index 0000000..ef695a9 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionRevertV2.sql @@ -0,0 +1,68 @@ +/****** Object: StoredProcedure [dbo].[extensionRevertV2] Script Date: 10.05.2026 15:26:24 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[extensionRevertV2] + @accountId INT, + @extensionToRemoveId INT +AS +BEGIN +SET NOCOUNT ON; +DECLARE @tExtId VARCHAR(20), @tCharId VARCHAR(20), @tNewLev VARCHAR(20), @tCurLev VARCHAR(20); + +DECLARE @characterId INT, @extensionId INT, @extensionLevel INT, @recId INT, @fee INT, @newLevel INT, @currentLevel INT; + +DECLARE exSpent CURSOR LOCAL FORWARD_ONLY FAST_FORWARD FOR +SELECT extensionid, extensionlevel, characterid, id FROM dbo.accountextensionspent WHERE accountid=@accountId AND extensionid=@extensionToRemoveId ORDER BY eventtime; +OPEN exSpent; +FETCH NEXT FROM exSpent INTO @extensionId,@extensionLevel,@characterId,@recId; +WHILE (@@FETCH_STATUS =0) +BEGIN + SET @tExtId = CAST(@extensionId AS VARCHAR(20)); + SET @tCharId = CAST(@characterId AS VARCHAR(20)); + + -- degrade the current extension level + IF (@extensionLevel = 1) + BEGIN + --remove character extension entry + DELETE dbo.characterextensions WHERE characterid=@characterId AND extensionid=@extensionId + + --pay back fee + SELECT @fee=price FROM extensions WHERE extensionid=@extensionId; + UPDATE characters SET credit=credit+@fee WHERE characterID=@characterId; + END + ELSE + BEGIN + + IF EXISTS (SELECT 1 FROM dbo.characterextensions WHERE characterid=@characterId AND extensionid=@extensionId) + BEGIN + + SET @newLevel = @extensionLevel-1; + SET @tNewLev = CAST(@newLevel AS VARCHAR(20)); + SET @currentLevel= (select extensionLevel FROM dbo.characterextensions WHERE characterid=@characterId AND extensionid=@extensionId); + SET @tCurLev = CAST(@currentLevel AS VARCHAR(20)); + + IF (@currentLevel IS NOT NULL AND @currentLevel > @newLevel) + BEGIN + UPDATE dbo.characterextensions SET extensionlevel=@newLevel WHERE characterid=@characterId AND extensionid=@extensionId + END + + END + + end + + -- remove the spending record + DELETE dbo.accountextensionspent WHERE id=@recId; + + FETCH NEXT FROM exSpent INTO @extensionId,@extensionLevel,@characterId,@recId; +END +CLOSE exSpent; DEALLOCATE exSpent; + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionSubscriptionStart.sql b/docs/db_structure/stored_procedures/extensionSubscriptionStart.sql new file mode 100644 index 0000000..1ff8279 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionSubscriptionStart.sql @@ -0,0 +1,22 @@ +/****** Object: StoredProcedure [dbo].[extensionSubscriptionStart] Script Date: 10.05.2026 15:34:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionSubscriptionStart] + + @accountID INT, + @startTime DATETIME, + @endTime DATETIME + +AS +BEGIN + + SET NOCOUNT ON; + + INSERT dbo.extensionsubscription ( accountid, starttime, endtime ) VALUES ( @accountid,@startTime,@endTime ) +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/extensionsRevertFromDate.sql b/docs/db_structure/stored_procedures/extensionsRevertFromDate.sql new file mode 100644 index 0000000..6323083 --- /dev/null +++ b/docs/db_structure/stored_procedures/extensionsRevertFromDate.sql @@ -0,0 +1,80 @@ +/****** Object: StoredProcedure [dbo].[extensionsRevertFromDate] Script Date: 10.05.2026 15:28:12 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[extensionsRevertFromDate] + @accountId INT, + @fromDate DATETIME +AS +BEGIN +SET NOCOUNT ON; +DECLARE @message VARCHAR(512), @tExtId VARCHAR(20), @tCharId VARCHAR(20), @tNewLev VARCHAR(20), @tCurLev VARCHAR(20), @epDiff INT; + +DECLARE @characterId INT, @extensionId INT, @extensionLevel INT, @recId INT, @fee INT, @preEp INT, @postEp INT, @newLevel INT, @currentLevel INT; +SET @preEp = dbo.extensionPointsAvailable(@accountId); + +DECLARE exSpent CURSOR LOCAL FORWARD_ONLY FAST_FORWARD FOR +SELECT extensionid, extensionlevel, characterid, id FROM dbo.accountextensionspent WHERE accountid=@accountId AND eventtime>@fromDate ORDER BY eventtime; +OPEN exSpent; +FETCH NEXT FROM exSpent INTO @extensionId,@extensionLevel,@characterId,@recId; +WHILE (@@FETCH_STATUS =0) +BEGIN + SET @tExtId = CAST(@extensionId AS VARCHAR(20)); + SET @tCharId = CAST(@characterId AS VARCHAR(20)); + + -- degrade the current extension level + IF (@extensionLevel = 1) + BEGIN + --remove character extension entry + DELETE dbo.characterextensions WHERE characterid=@characterId AND extensionid=@extensionId + SET @message = 'character extension deleted. extensionId:' + @tExtId + ' characterId:' + @tCharId ; RAISERROR( @message,0,1) WITH NOWAIT ; + + --pay back fee + SELECT @fee=price FROM extensions WHERE extensionid=@extensionId; + UPDATE characters SET credit=credit+@fee WHERE characterID=@characterId; + SET @message = 'extension fee returned. fee:' +CAST(@fee AS VARCHAR(20)) + ' characterId:' + @tCharId ; RAISERROR( @message,0,1) WITH NOWAIT ; + END + ELSE + BEGIN + + IF EXISTS (SELECT 1 FROM dbo.characterextensions WHERE characterid=@characterId AND extensionid=@extensionId) + BEGIN + + SET @newLevel = @extensionLevel-1; + SET @tNewLev = CAST(@newLevel AS VARCHAR(20)); + SET @currentLevel= (select extensionLevel FROM dbo.characterextensions WHERE characterid=@characterId AND extensionid=@extensionId); + SET @tCurLev = CAST(@currentLevel AS VARCHAR(20)); + + IF (@currentLevel IS NOT NULL AND @currentLevel > @newLevel) + BEGIN + UPDATE dbo.characterextensions SET extensionlevel=@newLevel WHERE characterid=@characterId AND extensionid=@extensionId + SET @message = 'downgrading ext:' + @tExtId + ' from:'+ @tCurLev + ' => ' + @tNewLev +' characterId:' + @tCharId ; RAISERROR( @message,0,1) WITH NOWAIT ; + END + ELSE + BEGIN + SET @message = 'nothing to do ext:' + @tExtId + ' current:'+ @tCurLev + ' new level:' + @tNewLev +' characterId:' + @tCharId ; RAISERROR( @message,0,1) WITH NOWAIT ; + END + + END + + end + + -- remove the spending record + DELETE dbo.accountextensionspent WHERE id=@recId; + SET @message = 'one spending entry done. ext:' + @tExtId + ' characterId:' + @tCharId ; RAISERROR( @message,0,1) WITH NOWAIT ; + + FETCH NEXT FROM exSpent INTO @extensionId,@extensionLevel,@characterId,@recId; +END +CLOSE exSpent; DEALLOCATE exSpent; +SET @postEp = dbo.extensionPointsAvailable(@accountId); +SET @epDiff = @postEp - @preEp; + +SELECT @accountId AS accountId, @preEp AS preEp, @postEp AS postEp, @epDiff AS epDiff +SET @message = 'preEp:' + CAST(@preEp AS VARCHAR(20)) + ' postEp:' + CAST(@postEp AS VARCHAR(20)) + ' epDiff:' + CAST(@epDiff AS VARCHAR(20)) ; RAISERROR( @message,0,1) WITH NOWAIT ; +SET @message = '-----' ; RAISERROR( @message,0,1) WITH NOWAIT ; +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/fieldTerminal_itemCount.sql b/docs/db_structure/stored_procedures/fieldTerminal_itemCount.sql new file mode 100644 index 0000000..0ef7224 --- /dev/null +++ b/docs/db_structure/stored_procedures/fieldTerminal_itemCount.sql @@ -0,0 +1,26 @@ +/****** Object: StoredProcedure [dbo].[fieldTerminal_itemCount] Script Date: 10.05.2026 15:36:54 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[fieldTerminal_itemCount] + + @zoneId INT, + @publicContainerDefinition int = 166, + @owner BIGINT + +AS +BEGIN + SET NOCOUNT ON; + + --innen erintetlen + + SELECT c.eid,COUNT(*) AS amount FROM dbo.getLiveDockingbaseChildren() c + JOIN entities i ON i.parent = c.eid and i.owner=@owner AND c.definition=@publicContainerDefinition + GROUP BY c.eid + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/findContainerRoot.sql b/docs/db_structure/stored_procedures/findContainerRoot.sql new file mode 100644 index 0000000..b911d69 --- /dev/null +++ b/docs/db_structure/stored_procedures/findContainerRoot.sql @@ -0,0 +1,42 @@ +/****** Object: StoredProcedure [dbo].[findContainerRoot] Script Date: 10.05.2026 15:53:14 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[findContainerRoot] + + @publicContainerDefinition int = 166, + @corporateHangarDefinition int = 581, + @itemEid bigint +AS +BEGIN + + + SET NOCOUNT ON; + + declare @parentEid bigint, @parentDefinition int + set @parentEid = @itemEid + set @parentDefinition = (select definition from entities where eid=@itemEid) + + while (@parentDefinition != @publicContainerDefinition AND @parentDefinition != @corporateHangarDefinition) + begin + set @itemEid = @parentEid + set @parentEid = (select parent from entities where eid=@itemEid) + set @parentDefinition = (select definition from entities where eid=@parentEid) + + if (@parentEid is null) + begin + select 0 + return + end + + + end + + select @parentEid + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/freshNewsCount.sql b/docs/db_structure/stored_procedures/freshNewsCount.sql new file mode 100644 index 0000000..aec3c08 --- /dev/null +++ b/docs/db_structure/stored_procedures/freshNewsCount.sql @@ -0,0 +1,37 @@ +/****** Object: StoredProcedure [dbo].[freshNewsCount] Script Date: 10.05.2026 15:55:03 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[freshNewsCount] + + @characterID int, + @language int + +AS +BEGIN + SET NOCOUNT ON; + + declare @lastLogOut smalldatetime + + if not exists (select characterID from characters where characterid=@characterID) + begin + select 0 + return + end + + set @lastLogOut = (select lastLogOut from characters where characterid=@characterID) + + if (@lastLogOut is null) + begin + select 0 + return + end + + SELECT count(*) from news where ntime > @lastLogOut and [language]=@language +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getAllChildrenEids.sql b/docs/db_structure/stored_procedures/getAllChildrenEids.sql new file mode 100644 index 0000000..5162ac8 --- /dev/null +++ b/docs/db_structure/stored_procedures/getAllChildrenEids.sql @@ -0,0 +1,34 @@ +/****** Object: StoredProcedure [dbo].[getAllChildrenEids] Script Date: 10.05.2026 15:59:59 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getAllChildrenEids] +( + @rootEID bigint -- the node which will be used as root +) + +AS + SET NOCOUNT ON; + +with children(eid,parent,lvl) + as + ( + SELECT eid,parent, 0 FROM entities WHERE eid = @rootEID + + UNION ALL + + SELECT C.eid,C.parent, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid + ) +select eid from children where eid<>@rootEID + +RETURN + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getEntityInfo.sql b/docs/db_structure/stored_procedures/getEntityInfo.sql new file mode 100644 index 0000000..9edd4be --- /dev/null +++ b/docs/db_structure/stored_procedures/getEntityInfo.sql @@ -0,0 +1,31 @@ +/****** Object: StoredProcedure [dbo].[getEntityInfo] Script Date: 10.05.2026 16:01:26 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +-- ============================================= +-- Author: +-- Create date: +-- Description: +-- ============================================= +CREATE PROCEDURE [dbo].[getEntityInfo] +( + @rootEID bigint +) +AS +BEGIN +with children(eid,definition,owner,parent, lvl) + as + ( + SELECT eid,definition,owner,parent, 0 FROM entities WHERE eid = @rootEID + + UNION ALL + + SELECT C.eid,C.definition,C.owner,C.parent, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid + ) + + select eid,definition,owner,parent from children option (MAXRECURSION 256) +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getExtensionBonus.sql b/docs/db_structure/stored_procedures/getExtensionBonus.sql new file mode 100644 index 0000000..459946f --- /dev/null +++ b/docs/db_structure/stored_procedures/getExtensionBonus.sql @@ -0,0 +1,24 @@ +/****** Object: StoredProcedure [dbo].[getExtensionBonus] Script Date: 10.05.2026 16:02:11 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +-- ============================================= +-- Author: +-- Create date: +-- Description: +-- ============================================= +CREATE PROCEDURE [dbo].[getExtensionBonus] + @characterid int +AS +BEGIN + SET NOCOUNT ON; + + select extensions.targetpropertyid as field, + sum(extensions.bonus * characterextensions.extensionlevel) as bonus + from characterextensions inner join extensions on characterextensions.extensionid = extensions.extensionid + where characterid = @characterid and extensions.targetpropertyid is not null group by extensions.targetpropertyid +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getFullRobot.sql b/docs/db_structure/stored_procedures/getFullRobot.sql new file mode 100644 index 0000000..b733692 --- /dev/null +++ b/docs/db_structure/stored_procedures/getFullRobot.sql @@ -0,0 +1,55 @@ +/****** Object: StoredProcedure [dbo].[getFullRobot] Script Date: 10.05.2026 16:02:44 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getFullRobot] +( + @robotEID bigint, -- the node which will be used as root + @cf bigint +) + +AS + SET NOCOUNT ON; + + + with children(eid,definition,owner,parent,health,ename,quantity,repackaged,dynprop, lvl) + as + ( + SELECT Q.eid,Q.definition,Q.owner,Q.parent,Q.health,Q.ename,Q.quantity,Q.repackaged,Q.dynprop, 0 FROM entities Q JOIN dbo.entitydefaults D ON Q.definition = D.definition AND (D.categoryflags & @cf) != @cf WHERE parent = @robotEID + + UNION ALL + + SELECT C.eid,C.definition,C.owner,C.parent,C.health,C.ename,C.quantity,C.repackaged,C.dynprop, M.lvl+1 + FROM entities AS C + JOIN children AS M ON C.parent = M.eid + + ) + + select eid,definition,owner,parent,health,ename,quantity,repackaged,dynprop from children + UNION ALL + select eid,definition,owner,parent,health,ename,quantity,repackaged,dynprop FROM dbo.entities WHERE eid=@robotEID + UNION ALL + select T.eid,T.definition,T.owner,T.parent,T.health,T.ename,T.quantity,T.repackaged,T.dynprop FROM dbo.entities T JOIN dbo.entitydefaults N ON T.definition=N.definition AND (N.categoryflags & @cf) = @cf WHERE T.parent=@robotEID + option (MAXRECURSION 2) + +RETURN + + + + + + + + + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getFullTree.sql b/docs/db_structure/stored_procedures/getFullTree.sql new file mode 100644 index 0000000..c32b481 --- /dev/null +++ b/docs/db_structure/stored_procedures/getFullTree.sql @@ -0,0 +1,47 @@ +/****** Object: StoredProcedure [dbo].[getFullTree] Script Date: 10.05.2026 16:03:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + + + + +CREATE PROCEDURE [dbo].[getFullTree] +( + @ownerEID bigint, + @rootEID bigint -- the node which will be used as root +) + + +--NEM HASZNALT, de meg nagyon jol johet + + +AS + SET NOCOUNT ON; + +with children(eid,definition,owner,parent,repackaged,quantity,health,ename, lvl) + as + ( + SELECT eid,definition,owner,parent,repackaged,quantity,health,ename, 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, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid where C.owner = @ownerEID + ) + + --select eid,definition,parent,repackaged,quantity,health,ename from children where eid<>@rootEID option (MAXRECURSION 256) +select eid from children where eid<>@rootEID + +RETURN + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getFullTreeByRoot.sql b/docs/db_structure/stored_procedures/getFullTreeByRoot.sql new file mode 100644 index 0000000..e86978f --- /dev/null +++ b/docs/db_structure/stored_procedures/getFullTreeByRoot.sql @@ -0,0 +1,37 @@ +/****** Object: StoredProcedure [dbo].[getFullTreeByRoot] Script Date: 10.05.2026 16:04:25 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getFullTreeByRoot] +( + @rootEID bigint -- the node which will be used as root +) + +AS + SET NOCOUNT ON; + +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 option (MAXRECURSION 7) + +RETURN + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getItemSummary.sql b/docs/db_structure/stored_procedures/getItemSummary.sql new file mode 100644 index 0000000..dbc8354 --- /dev/null +++ b/docs/db_structure/stored_procedures/getItemSummary.sql @@ -0,0 +1,39 @@ +/****** Object: StoredProcedure [dbo].[getItemSummary] Script Date: 10.05.2026 16:05:40 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getItemSummary] +( + @ownerEID bigint, + @rootEID bigint -- the node which will be used as root +) + +AS + SET NOCOUNT ON; + +with children(eid,definition,owner,parent,repackaged,quantity,health,ename, lvl) + as + ( + SELECT eid,definition,owner,parent,repackaged,quantity,health,ename, 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, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid where C.owner = @ownerEID + ) + +select i.definition,i.parent, SUM(CAST(i.quantity AS BIGINT)) from children i +where i.eid<>@rootEID GROUP BY i.definition,i.parent + + +RETURN + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getList.sql b/docs/db_structure/stored_procedures/getList.sql new file mode 100644 index 0000000..9e0d713 --- /dev/null +++ b/docs/db_structure/stored_procedures/getList.sql @@ -0,0 +1,67 @@ +/****** Object: StoredProcedure [dbo].[getList] Script Date: 10.05.2026 16:06:12 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getList] + + @containerCF BIGINT, + @rootEID BIGINT, + @ownerEID BIGINT =NULL, + @allItems BIT = 0, + @single BIT + +AS +BEGIN + +SET NOCOUNT ON; + +IF (@single = 1) +BEGIN + SELECT eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, 0 FROM entities WHERE eid = @rootEID + RETURN +END + + +IF (@ownerEID IS NULL) +BEGIN + SET @ownerEID=(SELECT owner FROM entities WHERE eid=@rooteid) +END; + + +with children(eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, lvl) + as + ( + --root + 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 with previous recursion if the parent is the same + JOIN children AS M ON C.parent = M.eid + + --if owner the character or the corporation + where (C.owner = @ownerEID OR @allitems = 1) + AND + + --add root to starct recursion and the parent is not a container = the containers will be listen, but not their content + (M.eid = @rootEID or M.definition NOT IN (SELECT definition FROM dbo.entitydefaults WHERE (categoryflags & 0xff)=@containerCF)) + ) + + select eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop,lvl from children option (MAXRECURSION 4) + + + + + +END + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getList2.sql b/docs/db_structure/stored_procedures/getList2.sql new file mode 100644 index 0000000..3fbf26d --- /dev/null +++ b/docs/db_structure/stored_procedures/getList2.sql @@ -0,0 +1,57 @@ +/****** Object: StoredProcedure [dbo].[getList2] Script Date: 10.05.2026 16:07:07 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getList2] + + @rootEID BIGINT, + @ownerEID BIGINT =NULL + +AS +BEGIN + +SET NOCOUNT ON; + +IF (@ownerEID IS NULL) +BEGIN + SET @ownerEID=(SELECT owner FROM entities WHERE eid=@rooteid) +END; + +with children(eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop, lvl) + as + ( + --root + 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 with previous recursion if the parent is the same + JOIN children AS M ON C.parent = M.eid + + --if owner the character or the corporation + where (C.owner = @ownerEID) + + ) + + select eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop,lvl from children option (MAXRECURSION 32) +-- select eid,definition,owner,parent,repackaged,quantity,health,ename,dynprop,lvl from children + + + + +END + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getMissionAverageTime.sql b/docs/db_structure/stored_procedures/getMissionAverageTime.sql new file mode 100644 index 0000000..d64c6c8 --- /dev/null +++ b/docs/db_structure/stored_procedures/getMissionAverageTime.sql @@ -0,0 +1,39 @@ +/****** Object: StoredProcedure [dbo].[getMissionAverageTime] Script Date: 10.05.2026 16:07:43 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getMissionAverageTime] + @missionID int +AS +BEGIN + + +SET NOCOUNT ON; + +DECLARE @missionDoneAmount INT + +SET @missionDoneAmount = (SELECT COUNT(*) FROM missionlog WHERE missionID=@missionID AND missionlog.finished IS NOT NULL AND missionlog.succeeded=1) + +IF @missionDoneAmount > 50 +BEGIN + + select + AVG(datediff(second,started,finished)) as seconds + from missionlog ml where succeeded=1 AND missionID=@missionID + +END + ELSE +BEGIN + SELECT -1 +END + + + + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getModulesFromRobot.sql b/docs/db_structure/stored_procedures/getModulesFromRobot.sql new file mode 100644 index 0000000..4d07168 --- /dev/null +++ b/docs/db_structure/stored_procedures/getModulesFromRobot.sql @@ -0,0 +1,30 @@ +/****** Object: StoredProcedure [dbo].[getModulesFromRobot] Script Date: 10.05.2026 16:09:27 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getModulesFromRobot] +( + @robotEID as bigint, + @categoryFlags as bigint +) +AS +BEGIN + with children(eid,definition, parent, lvl) +as +( + select eid,definition, parent, 0 FROM entities WHERE eid = @robotEID + + union ALL + + select C.eid,C.definition, C.parent, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid +) + + +select children.definition from children left outer join entitydefaults on children.definition = entitydefaults.definition where eid<>@robotEID and (entitydefaults.categoryflags & 255) = 19 option (MAXRECURSION 2) +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getStructureRoot.sql b/docs/db_structure/stored_procedures/getStructureRoot.sql new file mode 100644 index 0000000..7f58f68 --- /dev/null +++ b/docs/db_structure/stored_procedures/getStructureRoot.sql @@ -0,0 +1,38 @@ +/****** Object: StoredProcedure [dbo].[getStructureRoot] Script Date: 10.05.2026 16:28:41 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getStructureRoot] + + @eid bigint + +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @currentParent BIGINT, @currentEid BIGINT + SET @currentParent = (SELECT parent FROM entities WHERE eid=@eid) + + IF @currentParent is NULL OR @currentParent = 0 + BEGIN + SELECT 0,(SELECT definition FROM entities WHERE eid=@eid) + return + END + + + + WHILE @currentParent IS NOT NULL OR @currentParent > 0 + BEGIN + SELECT @currentParent=parent,@currentEid=eid FROM entities WHERE eid=@currentParent + END; + + + SELECT @currentEid,definition FROM entities WHERE eid=@currentEid + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getTableColumnInfo.sql b/docs/db_structure/stored_procedures/getTableColumnInfo.sql new file mode 100644 index 0000000..8b70695 --- /dev/null +++ b/docs/db_structure/stored_procedures/getTableColumnInfo.sql @@ -0,0 +1,20 @@ +/****** Object: StoredProcedure [dbo].[getTableColumnInfo] Script Date: 10.05.2026 16:29:28 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[getTableColumnInfo] + ( + @table_name varchar(384) + ) + + --retrieves all info about the specified table's columns + +AS + SET NOCOUNT ON; + + exec sp_columns @table_name + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getTreeEIDs.sql b/docs/db_structure/stored_procedures/getTreeEIDs.sql new file mode 100644 index 0000000..2bad06a --- /dev/null +++ b/docs/db_structure/stored_procedures/getTreeEIDs.sql @@ -0,0 +1,34 @@ +/****** Object: StoredProcedure [dbo].[getTreeEIDs] Script Date: 10.05.2026 16:30:07 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[getTreeEIDs] +( + @rootEID bigint -- the node which will be used as root +) + +AS + SET NOCOUNT ON; + +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 + + +RETURN + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getTreeNonFiltered.sql b/docs/db_structure/stored_procedures/getTreeNonFiltered.sql new file mode 100644 index 0000000..3d5ae6b --- /dev/null +++ b/docs/db_structure/stored_procedures/getTreeNonFiltered.sql @@ -0,0 +1,40 @@ +/****** Object: StoredProcedure [dbo].[getTreeNonFiltered] Script Date: 10.05.2026 16:30:39 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[getTreeNonFiltered] +( + @rootEID bigint -- the node which will be used as root +) + + + +AS + SET NOCOUNT ON; + +with children(eid,definition,owner,parent,repackaged,quantity,health,ename, lvl) + as + ( + SELECT eid,definition,owner,parent,repackaged,quantity,health,ename, 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, M.lvl+1 FROM entities AS C JOIN children AS M ON C.parent = M.eid + ) + + select eid,owner,definition,parent,repackaged,quantity,health,ename from children where eid<>@rootEID + + +RETURN + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/getcorporationstrength.sql b/docs/db_structure/stored_procedures/getcorporationstrength.sql new file mode 100644 index 0000000..609f174 --- /dev/null +++ b/docs/db_structure/stored_procedures/getcorporationstrength.sql @@ -0,0 +1,26 @@ +/****** Object: StoredProcedure [dbo].[getcorporationstrength] Script Date: 10.05.2026 16:00:47 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[getcorporationstrength] + + @corporationEID bigint + +AS +BEGIN + + + SET NOCOUNT ON; + + +SELECT SUM(points) FROM accountextensionspent WHERE characterid in + +( +SELECT characterid FROM characters WHERE active=1 AND characterID IN (SELECT memberid FROM corporationmembers WHERE corporationEID=@corporationEID) +) + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/increaseExtensionLevel.sql b/docs/db_structure/stored_procedures/increaseExtensionLevel.sql new file mode 100644 index 0000000..bbc86e3 --- /dev/null +++ b/docs/db_structure/stored_procedures/increaseExtensionLevel.sql @@ -0,0 +1,34 @@ +/****** Object: StoredProcedure [dbo].[increaseExtensionLevel] Script Date: 10.05.2026 16:31:42 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[increaseExtensionLevel] +( + @characterID as int, + @extensionID as INT, + @extensionLevel AS INT +) +AS +BEGIN + + +if exists (select characterextensionid from characterExtensions where characterid = @characterID and extensionID = @extensionID) +begin + update characterextensions set extensionlevel = @extensionLevel where characterid = @characterID and extensionid = @extensionID +end +else +begin + insert into characterextensions (characterid,extensionid,extensionlevel) values (@characterID,@extensionID,1) +end + +select @@ROWCOUNT + + +END + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/indexesMaintenance.sql b/docs/db_structure/stored_procedures/indexesMaintenance.sql new file mode 100644 index 0000000..f559e77 --- /dev/null +++ b/docs/db_structure/stored_procedures/indexesMaintenance.sql @@ -0,0 +1,67 @@ +/****** Object: StoredProcedure [dbo].[indexesMaintenance] Script Date: 10.05.2026 16:37:15 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +CREATE PROCEDURE [dbo].[indexesMaintenance] +AS +BEGIN + + SET NOCOUNT ON + + CREATE TABLE #Fragmentation + ( + TableName NVARCHAR(200), + IndexName NVARCHAR(200), + FragmentationAmount DECIMAL(18,4) + ) + + -- Load all of the fragmented tables + INSERT INTO #Fragmentation (TableName, IndexName, FragmentationAmount) + SELECT DISTINCT + TableName = S.name + '.' + tbl.[name], + IndexName = ind.name, + FragmentationAmount = MAX(mn.avg_fragmentation_in_percent) + FROM sys.dm_db_index_physical_stats(NULL, NULL, NULL, NULL, NULL) AS mn + INNER JOIN sys.tables tbl ON tbl.[object_id] = mn.[object_id] + INNER JOIN sys.indexes ind ON ind.[object_id] = mn.[object_id] + INNER JOIN sys.schemas S ON tbl.schema_id = S.schema_id + WHERE [database_id] = DB_ID() AND + mn.avg_fragmentation_in_percent > 5 AND + ind.type_desc <> 'NONCLUSTERED COLUMNSTORE' AND + ind.name IS NOT NULL + GROUP BY S.name + '.' + tbl.[name], ind.name + ORDER BY MAX(mn.avg_fragmentation_in_percent) DESC + + DECLARE @tableName NVARCHAR(200) + DECLARE @indexName NVARCHAR(200) + DECLARE @fragmentationAmount DECIMAL(18,4) + DECLARE @sql VARCHAR(1000) + + DECLARE curse CURSOR FAST_FORWARD READ_ONLY FOR + SELECT TableName, IndexName, FragmentationAmount FROM #Fragmentation + + OPEN curse + + FETCH NEXT FROM curse INTO @tableName, @indexName, @fragmentationAmount + + WHILE @@FETCH_STATUS = 0 + BEGIN + SET @sql = 'ALTER INDEX ' + @IndexName + ' ON ' + @TableName + CASE WHEN @FragmentationAmount > 30 THEN ' REBUILD' ELSE ' REORGANIZE' END + + PRINT @sql + EXEC(@sql) + + FETCH NEXT FROM curse INTO @tableName, @indexName, @fragmentationAmount + END + + CLOSE curse + DEALLOCATE curse + + DROP TABLE #Fragmentation + +END + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/initServer.sql b/docs/db_structure/stored_procedures/initServer.sql new file mode 100644 index 0000000..804111d --- /dev/null +++ b/docs/db_structure/stored_procedures/initServer.sql @@ -0,0 +1,61 @@ +/****** Object: StoredProcedure [dbo].[initServer] Script Date: 10.05.2026 16:38:30 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + + +CREATE PROCEDURE [dbo].[initServer] + /* + cleans the runtime tables + */ +AS + SET NOCOUNT ON +/* +DBCC TRACEON(1118,-1) +DBCC TRACEON(2371,-1) +DBCC TRACEON(3213,-1) +DBCC TRACEON(3226,-1) +DBCC TRACEON(3604,-1) +*/ + + --set every account's isloggedin + update accounts set isloggedin=0 + + --set every character to unselected + update characters set inuse=0 + + -- cleanup channels + EXEC dbo.deleteUnusedPublicChannels + + -- clean up channelmembers + delete from channelmembers where memberid in (select characterid from characters where active = 0) + + UPDATE dbo.intrusionsites SET intrusionstarttime=NULL WHERE intrusionstarttime<=DATEADD(MINUTE,10,getdate()) AND intrusionstarttime IS NOT null + + + delete from dbo.zoneuserentities WHERE eid NOT in + (SELECT eid FROM dbo.entities) + + + EXEC dbo.missionCleanUpLog + + DELETE dbo.zoneuserentities WHERE eid NOT IN (SELECT eid FROM entities) + DELETE dbo.pbsconnections WHERE (sourceeid NOT IN (SELECT eid FROM zoneuserentities)) OR (targeteid NOT IN (SELECT eid FROM zoneuserentities)) + DELETE dbo.marketitems WHERE isSell=1 AND isvendoritem=0 AND itemeid NOT IN (SELECT eid FROM dbo.entities) + + + RETURN + + + + + + + + + + +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/initchannels.sql b/docs/db_structure/stored_procedures/initchannels.sql new file mode 100644 index 0000000..de53718 --- /dev/null +++ b/docs/db_structure/stored_procedures/initchannels.sql @@ -0,0 +1,55 @@ +/****** Object: StoredProcedure [dbo].[initchannels] Script Date: 10.05.2026 16:37:53 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[initchannels] + +AS +BEGIN + + +SET NOCOUNT ON; + +--delete all channels +DELETE dbo.channels + +--create corporation channels +INSERT channels ([name],[password],[topic],[type]) + +SELECT 'corporation_' + CAST(c.eid AS VARCHAR(16)), +NULL, +w.missionstatement, +2 +FROM corporations c left JOIN dbo.cw_corporation w ON c.eid=w.corporationEID WHERE c.active=1 + +--create station channels +INSERT channels ([name],[password],[topic],[type]) +SELECT 'base_' + CAST(eid AS VARCHAR(16)), +NULL, +NULL, +4 +FROM dbo.zoneentities WHERE eid in (SELECT eid FROM dbo.entities WHERE definition in (SELECT definition FROM dbo.entitydefaults where definitionname LIKE '%docking%')) + +TRUNCATE TABLE dbo.channelmembers + +--create corp channel members +INSERT dbo.channelmembers ( memberid, channelid,[role]) +SELECT memberid, +(SELECT id FROM channels WHERE [name]= 'corporation_' + CAST(cm.corporationEID AS VARCHAR(16) )), +(CASE ([role] & 19) WHEN 0 THEN 0 ELSE 2 end) +from dbo.corporationmembers cm JOIN characters c ON cm.memberid = c.characterID WHERE c.active=1 + +--station channel members +INSERT dbo.channelmembers ( memberid, channelid,[role]) +SELECT (SELECT id FROM channels WHERE [name] ='base_' + CAST(baseEID AS VARCHAR(16))), +characterid, +0 +FROM characters WHERE active=1 + + +END +GO \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/insertAveragePrice.sql b/docs/db_structure/stored_procedures/insertAveragePrice.sql new file mode 100644 index 0000000..a7fb48a --- /dev/null +++ b/docs/db_structure/stored_procedures/insertAveragePrice.sql @@ -0,0 +1,57 @@ +/****** Object: StoredProcedure [dbo].[insertAveragePrice] Script Date: 10.05.2026 16:40:10 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + + +CREATE PROCEDURE [dbo].[insertAveragePrice] +( + @marketEID bigint, + @itemDefinition int, + @price float, + @quantity bigint, + @date datetime +) +AS +BEGIN + +declare @perPiecePrice float, @currentLowest float, @currentHighest float + +set @perPiecePrice = @price / @quantity + + +if not exists (select marketeid from marketaverageprices where marketeid = @marketEID and itemdefinition = @itemdefinition and date = @date) +begin + + insert into marketaverageprices (marketeid,itemdefinition,totalprice,quantity,date,dailylowest,dailyhighest) + values (@marketEID,@itemDefinition,@price,@quantity,@date,@perPiecePrice,@perPiecePrice) + +end else +begin + + select @currentLowest=dailylowest, @currentHighest=dailyhighest + from marketaverageprices + where marketeid = @marketEID and itemdefinition = @itemdefinition and date = @date + + if (@currentLowest > @perPiecePrice) + begin + set @currentLowest = @perPiecePrice + end + + if (@currentHighest < @perPiecePrice) + begin + set @currentHighest = @perPiecePrice + end + + update marketaverageprices + set + totalprice = totalprice + @price, + quantity = quantity + @quantity, + dailylowest = @currentLowest, + dailyhighest = @currentHighest + where marketeid = @marketEID and itemdefinition = @itemdefinition and date = @date +end + +END \ No newline at end of file diff --git a/docs/db_structure/stored_procedures/insertMarketItem.sql b/docs/db_structure/stored_procedures/insertMarketItem.sql new file mode 100644 index 0000000..e69de29 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 From 8fbeb5b80cc1bd0d7d881fcbb2215c7d4285537c Mon Sep 17 00:00:00 2001 From: "Dolzhukov, Viktor" Date: Sun, 10 May 2026 18:35:08 +0500 Subject: [PATCH 03/11] fix(seasons): notify online players in RefreshCache on season start transition --- .../Services/Seasons/SeasonService.cs | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/Perpetuum/Services/Seasons/SeasonService.cs diff --git a/src/Perpetuum/Services/Seasons/SeasonService.cs b/src/Perpetuum/Services/Seasons/SeasonService.cs new file mode 100644 index 0000000..9cae8b8 --- /dev/null +++ b/src/Perpetuum/Services/Seasons/SeasonService.cs @@ -0,0 +1,335 @@ +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; + +namespace Perpetuum.Services.Seasons +{ + public class SeasonService : Process, ISeasonService + { + private static readonly TimeSpan CacheRefreshInterval = TimeSpan.FromMinutes(5); + + private const string AnnouncerNick = "[OPP] Announcer"; + + private readonly SeasonRepository _repository; + private readonly ISessionManager _sessionManager; + private readonly ICustomDictionary _customDictionary; + private readonly Lazy _announcer = new(() => Character.GetByNick(AnnouncerNick)); + + // 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. 0 = never notified. + private volatile int _lastNotifiedSeasonId; + + // Trigger immediate load on first Update tick + private TimeSpan _cacheAge = CacheRefreshInterval; + + public SeasonService(SeasonRepository repository, ISessionManager sessionManager, + ICustomDictionary customDictionary) + { + _repository = repository; + _sessionManager = sessionManager; + _customDictionary = customDictionary; + _sessionManager.SessionAdded += OnSessionAdded; + } + + private void OnSessionAdded(ISession session) + { + session.CharacterSelected += (_, character) => OnCharacterLogin(character); + } + + // ── 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); + } + + 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); + } + } + + // ── 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 — check all unclaimed tiers now reachable + 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) + { + // Null the cache immediately so no further activity is recorded + _activeSeason = null; + _lastNotifiedSeasonId = 0; + _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); + } + + _activeRates = ImmutableList.Empty; + _activeObjectives = ImmutableList.Empty; + _activeTiers = ImmutableList.Empty; + _activeLeaderboard = ImmutableList.Empty; + } + + // ── Mail helpers ───────────────────────────────────────────────────── + + 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 _); + } + + private 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(_announcer.Value, character, subject, body, + MailType.character, out _, out _); + } + + private 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!\n" + + $"Total points: {total}\n" + + $"Redeem your reward at any terminal via the Redeemable Items menu."; + MailHandler.SendMail(_announcer.Value, character, subject, body, + MailType.character, out _, out _); + } + + private 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 '{seasonName}' 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(_announcer.Value, character, subject, body, + MailType.character, out _, out _); + } + + public void SendActivationMailToOnlineCharacters(Season season) + { + RefreshCache(); + } + + 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); + } + } + + // ── Helpers ────────────────────────────────────────────────────────── + + 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(), + }; + } +} From 4b9a498865531f8f59ce08141f05fc5e13076806 Mon Sep 17 00:00:00 2001 From: "Dolzhukov, Viktor" Date: Sun, 10 May 2026 19:09:29 +0500 Subject: [PATCH 04/11] feat(seasons): complete Seasons feature with schema docs and start-notification fix - Add 8 new tables to database_schema_documentation.md (seasons, season_activity_rates, season_objectives, season_tiers, season_leaderboard_rewards, season_character_points, season_objective_progress, season_tier_claims) with columns, indexes, and relations - Update Table of Contents with new season table entries in alphabetical order - Mark seasons design specs as Implemented - Include all Seasons system source files, admin command handlers, bootstrapper wiring, and activity recording hooks across Player, Npc, Outpost, Driller modules Co-Authored-By: Claude Sonnet 4.6 --- .../p36.0/Features/Seasons/migration.sql | 86 + .../database_schema_documentation.md | 220 ++ ...26-05-10-season-intro-mail-improvements.md | 373 ++++ .../2026-05-10-season-start-notification.md | 223 ++ .../plans/2026-05-10-seasons-system.md | 1908 +++++++++++++++++ ...-05-10-season-start-notification-design.md | 60 + .../specs/2026-05-10-seasons-design.md | 2 +- .../Modules/SeasonModule.cs | 26 + .../PerpetuumBootstrapper.cs | 1 + src/Perpetuum/Accounting/AccountManager.cs | 3 + .../Accounting/Characters/CharacterWallet.cs | 7 + src/Perpetuum/Modules/DrillerModule.cs | 2 + src/Perpetuum/Modules/LargeDrillerModule.cs | 2 + src/Perpetuum/Players/Player.cs | 4 + .../ChatCommands/GameAdminCommands.cs | 4 +- .../SeasonAdminCommandHandlers.cs | 147 ++ .../MissionProcessorAdvanceTarget.cs | 7 + .../Services/Seasons/ISeasonService.cs | 10 + .../Services/Seasons/SeasonActivityType.cs | 14 + .../Services/Seasons/SeasonModels.cs | 70 + .../Services/Seasons/SeasonRepository.cs | 436 ++++ .../Services/Seasons/SeasonServiceLocator.cs | 7 + src/Perpetuum/Zones/Intrusion/Outpost.cs | 2 + src/Perpetuum/Zones/NpcSystem/Npc.cs | 4 + 24 files changed, 3616 insertions(+), 2 deletions(-) create mode 100644 docs/Patches/p36.0/Features/Seasons/migration.sql create mode 100644 docs/superpowers/plans/2026-05-10-season-intro-mail-improvements.md create mode 100644 docs/superpowers/plans/2026-05-10-season-start-notification.md create mode 100644 docs/superpowers/plans/2026-05-10-seasons-system.md create mode 100644 docs/superpowers/specs/2026-05-10-season-start-notification-design.md create mode 100644 src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs create mode 100644 src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs create mode 100644 src/Perpetuum/Services/Seasons/ISeasonService.cs create mode 100644 src/Perpetuum/Services/Seasons/SeasonActivityType.cs create mode 100644 src/Perpetuum/Services/Seasons/SeasonModels.cs create mode 100644 src/Perpetuum/Services/Seasons/SeasonRepository.cs create mode 100644 src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs 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..37941bb --- /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 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); diff --git a/docs/db_structure/database_schema_documentation.md b/docs/db_structure/database_schema_documentation.md index ff9f786..6e9aa3e 100644 --- a/docs/db_structure/database_schema_documentation.md +++ b/docs/db_structure/database_schema_documentation.md @@ -258,6 +258,14 @@ Generated from DBML structure. - [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) @@ -5977,6 +5985,218 @@ Generated from DBML structure. --- +## 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` | `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]` | + +### 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` | `bigint [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` 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-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 + [ChatCommand("SeasonDeactivate")] + public static void SeasonDeactivate(AdminCommandData data) + { + CheckRequiredArgLength(data, 1); + if (!int.TryParse(data.Command.Args[0], out int id)) + { + SendMessageToAll(data, "Usage: #SeasonDeactivate "); + return; + } + + var repo = new SeasonRepository(); + repo.SetSeasonActive(id, false); + SendMessageToAll(data, $"Season {id} deactivated."); + } + + // #SeasonAddRate + // activityType: 1=NpcKill 2=PvpKill 3=Mission 4=Mineral 5=EpSpent 6=NicEarned 7=NicSpent 8=Intrusion + // Example: #SeasonAddRate 1 1 10 1 (10 pts per NPC kill) + // Example: #SeasonAddRate 1 6 1 1000 (1 pt per 1000 NIC earned) + [ChatCommand("SeasonAddRate")] + public static void SeasonAddRate(AdminCommandData data) + { + CheckRequiredArgLength(data, 4); + if (!int.TryParse(data.Command.Args[0], out int seasonId) || + !int.TryParse(data.Command.Args[1], out int actType) || + !double.TryParse(data.Command.Args[2], NumberStyles.Float, + CultureInfo.InvariantCulture, out double pts) || + !int.TryParse(data.Command.Args[3], out int scale)) + { + SendMessageToAll(data, "Usage: #SeasonAddRate "); + return; + } + + var repo = new SeasonRepository(); + repo.AddActivityRate(seasonId, (SeasonActivityType)actType, pts, scale); + SendMessageToAll(data, $"Added rate to season {seasonId}: type={actType} pts={pts} scale={scale}"); + } + + // #SeasonAddObjective + // Example: #SeasonAddObjective 1 1 50 500 KillFrenzy + [ChatCommand("SeasonAddObjective")] + public static void SeasonAddObjective(AdminCommandData data) + { + CheckRequiredArgLength(data, 5); + if (!int.TryParse(data.Command.Args[0], out int seasonId) || + !int.TryParse(data.Command.Args[1], out int actType) || + !long.TryParse(data.Command.Args[2], out long target) || + !int.TryParse(data.Command.Args[3], out int bonus)) + { + SendMessageToAll(data, "Usage: #SeasonAddObjective "); + return; + } + + string name = data.Command.Args[4]; + var repo = new SeasonRepository(); + repo.AddObjective(seasonId, (SeasonActivityType)actType, target, bonus, name, ""); + SendMessageToAll(data, $"Added objective '{name}' to season {seasonId}: type={actType} target={target} bonus={bonus}"); + } + + // #SeasonAddTier + // Example: #SeasonAddTier 1 1 Bronze 1000 42 + [ChatCommand("SeasonAddTier")] + public static void SeasonAddTier(AdminCommandData data) + { + CheckRequiredArgLength(data, 5); + if (!int.TryParse(data.Command.Args[0], out int seasonId) || + !int.TryParse(data.Command.Args[1], out int tierNum) || + !int.TryParse(data.Command.Args[3], out int pts) || + !int.TryParse(data.Command.Args[4], out int pkgId)) + { + SendMessageToAll(data, "Usage: #SeasonAddTier "); + return; + } + + string name = data.Command.Args[2]; + var repo = new SeasonRepository(); + repo.AddTier(seasonId, tierNum, name, pts, pkgId); + SendMessageToAll(data, $"Added tier '{name}' (#{tierNum}) to season {seasonId}: {pts} pts, package {pkgId}"); + } + + // #SeasonAddLeaderboard + // Example: #SeasonAddLeaderboard 1 1 1 99 (first place gets package 99) + [ChatCommand("SeasonAddLeaderboard")] + public static void SeasonAddLeaderboard(AdminCommandData data) + { + CheckRequiredArgLength(data, 4); + if (!int.TryParse(data.Command.Args[0], out int seasonId) || + !int.TryParse(data.Command.Args[1], out int rankMin) || + !int.TryParse(data.Command.Args[2], out int rankMax) || + !int.TryParse(data.Command.Args[3], out int pkgId)) + { + SendMessageToAll(data, "Usage: #SeasonAddLeaderboard "); + return; + } + + var repo = new SeasonRepository(); + repo.AddLeaderboardReward(seasonId, rankMin, rankMax, pkgId); + SendMessageToAll(data, $"Added leaderboard reward to season {seasonId}: ranks {rankMin}-{rankMax}, package {pkgId}"); + } + + // #SeasonStatus + [ChatCommand("SeasonStatus")] + public static void SeasonStatus(AdminCommandData data) + { + var repo = new SeasonRepository(); + var (name, remaining, count) = repo.GetSeasonStatus(); + + if (name == "(none)") + { + SendMessageToAll(data, "No active season."); + return; + } + + string timeStr = remaining > TimeSpan.Zero + ? $"{(int)remaining.TotalHours}h {remaining.Minutes}m remaining" + : "ENDED (pending processing)"; + + SendMessageToAll(data, $"Season: '{name}' | {timeStr} | {count} participants"); + } + + // #SeasonForceEnd + [ChatCommand("SeasonForceEnd")] + public static void SeasonForceEnd(AdminCommandData data) + { + CheckRequiredArgLength(data, 1); + if (!int.TryParse(data.Command.Args[0], out int id)) + { + SendMessageToAll(data, "Usage: #SeasonForceEnd "); + return; + } + + // Force end by moving end_time to now-1min so the service timer triggers on next tick + Db.Query() + .CommandText("UPDATE seasons SET end_time = DATEADD(MINUTE, -1, GETUTCDATE()) WHERE id = @id") + .SetParameter("@id", id) + .ExecuteNonQuery(); + + SendMessageToAll(data, $"Season {id} end_time set to now. End-of-season processing will run within 1 minute."); + } + + // ── Helpers ─────────────────────────────────────────────────────────── + + private static void CheckRequiredArgLength(AdminCommandData data, int required) + { + if (data.Command.Args == null || data.Command.Args.Length < required) + throw PerpetuumException.Create(ErrorCodes.RequiredArgumentIsNotSpecified); + } + + private static void SendMessageToAll(AdminCommandData data, string message) + { + data.Channel.SendMessageToAll(data.SessionManager, data.Sender, message); + } + } +} +``` + +- [ ] **Step 2: Build to verify** + +```bash +dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64 +``` + +Fix any compilation errors — likely missing `using Perpetuum.Data;` for the `Db.Query()` call in `SeasonForceEnd`, or namespace mismatches. Check how `AdminCommandHandlers.cs` includes its usings as a reference. + +- [ ] **Step 3: Commit** + +```bash +git add src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs +git commit -m "feat(seasons): add admin chat command handlers for season management" +``` + +--- + +## Task 16: Register Admin Handlers in Bootstrapper + +**Files:** +- Modify: `src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs` + +Admin command handlers are discovered by reflection. Find where `AdminCommandHandlers` is registered and add `SeasonAdminCommandHandlers` alongside it. + +- [ ] **Step 1: Find how admin handlers are registered** + +Search `PerpetuumBootstrapper.cs` for `AdminCommandHandlers` or `ChatCommand`. The bootstrapper likely calls something like: + +```csharp +ChatCommandService.RegisterHandlers(typeof(AdminCommandHandlers)); +``` + +Or it may scan an assembly. Find the exact call. + +- [ ] **Step 2: Add `SeasonAdminCommandHandlers` registration** + +Add the same registration pattern for `SeasonAdminCommandHandlers`: + +```csharp +ChatCommandService.RegisterHandlers(typeof(SeasonAdminCommandHandlers)); +``` + +Or if it's assembly-scanning, ensure `SeasonAdminCommandHandlers` is in the scanned assembly (it already is, since it's in `Perpetuum`). + +- [ ] **Step 3: Build to verify** + +```bash +dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64 +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs +git commit -m "feat(seasons): register SeasonAdminCommandHandlers in bootstrapper" +``` + +--- + +## Task 17: Build Verification and Manual Test Checklist + +- [ ] **Step 1: Full clean build** + +```bash +dotnet build PerpetuumServer2.sln -c Release -p:Platform=x64 --no-incremental +``` + +Expected: zero errors, zero warnings introduced by this feature. + +- [ ] **Step 2: Run the migration script** + +Apply `docs/Patches/p36.0/Features/Seasons/migration.sql` to the game database. Verify all 8 tables exist. + +- [ ] **Step 3: Start the server and verify startup** + +Start the server and check logs for: +- No exceptions during bootstrapper initialization +- `SeasonModule` loads without error +- `SeasonService` starts (process manager tick fires without exception) + +- [ ] **Step 4: Create a test season via admin command** + +In the secured admin chat channel: +``` +#Secure +#SeasonCreate TestSeason 2026-05-10 2026-12-31 +``` +Expected reply: `Season created. ID=1 Name='TestSeason' ...` + +- [ ] **Step 5: Add a rate and tier** + +``` +#SeasonAddRate 1 1 10 1 +#SeasonAddTier 1 1 Bronze 5 +``` +(Use a `packageid` that exists in your `packages` table.) + +- [ ] **Step 6: Activate the season** + +``` +#SeasonActivate 1 +``` +Expected: online players receive intro mail. + +- [ ] **Step 7: Kill an NPC in-game** + +Check `season_character_points` for the character row: +```sql +SELECT * FROM season_character_points WHERE season_id = 1; +``` +Expected: `total_points = 10` (or whatever your rate is). + +- [ ] **Step 8: Verify tier delivery** + +Kill enough NPCs to cross the Bronze tier (50 kills × 10 pts = 500 pts — adjust as needed). Check: +- `season_tier_claims` has a row for the character +- `accountredeemableitems` has the reward rows +- Character received a tier-unlock mail + +- [ ] **Step 9: Test SeasonStatus** + +``` +#SeasonStatus +``` +Expected: shows season name, time remaining, participant count. + +- [ ] **Step 10: Test ForceEnd** + +``` +#SeasonForceEnd 1 +``` +Wait up to 1 minute. Check: +- `seasons.is_active = 0` +- Participants received final-standings mail +- Top-ranked characters have rows in `accountredeemableitems` for leaderboard rewards (if leaderboard rewards were configured) + +--- + +## Summary of All New Files + +``` +docs/Patches/p36.0/Features/Seasons/migration.sql +src/Perpetuum/Services/Seasons/SeasonActivityType.cs +src/Perpetuum/Services/Seasons/SeasonModels.cs +src/Perpetuum/Services/Seasons/ISeasonService.cs +src/Perpetuum/Services/Seasons/SeasonRepository.cs +src/Perpetuum/Services/Seasons/SeasonService.cs +src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs +src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs +src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs +``` + +## Summary of Modified Files + +``` +src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs (SeasonModule + admin handlers) +src/Perpetuum/Zones/NpcSystem/Npc.cs (NPC kill hook) +src/Perpetuum/Players/Player.cs (PvP kill hook + login hook) +src/Perpetuum/Services/MissionEngine/…/MissionProcessorAdvanceTarget.cs (mission hook) +src/Perpetuum/Accounting/Characters/CharacterWallet.cs (NIC hook) +src/Perpetuum/Accounting/AccountManager.cs (EP spent hook) +src/Perpetuum/Zones/Intrusion/Outpost.cs (intrusion hook) +src/Perpetuum/Modules/DrillerModule.cs (mineral hook) +src/Perpetuum/Modules/LargeDrillerModule.cs (mineral hook) +``` diff --git a/docs/superpowers/specs/2026-05-10-season-start-notification-design.md b/docs/superpowers/specs/2026-05-10-season-start-notification-design.md new file mode 100644 index 0000000..7a6064c --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-season-start-notification-design.md @@ -0,0 +1,60 @@ +# Season Start Notification Fix — Design Spec + +**Date:** 2026-05-10 +**Status:** Implemented + +## Problem + +When a season becomes active, online players do not receive the season intro email if `RefreshCache()` in the Update loop detects the new season before (or instead of) the `#SeasonActivate` admin command calling `SendActivationMailToOnlineCharacters`. The notification logic is split: the admin command path sends emails, but the Update loop's periodic cache refresh silently absorbs a new season without notifying anyone. + +## Root Cause + +`RefreshCache()` is the single authoritative reader of live season state. It sets `_activeSeason` whenever a season is found active in the DB, but takes no action on the null → non-null transition. The admin command compensates by calling `SendActivationMailToOnlineCharacters` manually, but this creates two code paths and a race window. + +## Approach + +Move the notification trigger into `RefreshCache()`. Add a `_lastNotifiedSeasonId` (nullable `int`) field that tracks which season the intro mail has already been dispatched for. On each cache refresh, if the active season ID differs from `_lastNotifiedSeasonId`, iterate `SelectedCharacters` and send intro mail via the existing `TryMarkIntroMailSent` + `SendIntroMail` pair. Update `_lastNotifiedSeasonId` afterward. + +`SendActivationMailToOnlineCharacters` is simplified to just call `RefreshCache()`, which now handles everything. The admin command still provides eager triggering (no 5-minute wait), but the logic lives in one place. + +## Data Flow + +``` +#SeasonActivate (admin) Update loop (every 1 min) + │ │ + ▼ ▼ +SetSeasonActive() [DB] _cacheAge >= 5 min + │ │ + ▼ ▼ +SendActivationMailToOnlineCharacters() + │ + ▼ + RefreshCache() + │ + ├─ GetActiveSeason() → season (or null) + ├─ if season.Id != _lastNotifiedSeasonId: + │ iterate SelectedCharacters + │ TryMarkIntroMailSent() [DB atomic] ← idempotent guard + │ SendIntroMail() if first time + │ _lastNotifiedSeasonId = season.Id + └─ _activeSeason = season +``` + +## Idempotency + +`TryMarkIntroMailSent` is a DB-level atomic `UPDATE WHERE intro_mail_sent = 0`, so concurrent calls from the admin command thread and the Update loop thread cannot double-deliver. Even if `RefreshCache()` is called twice in rapid succession (race between Update loop and admin command), only the first call sends the mail per character. + +## Files Changed + +| File | Change | +|---|---| +| `src/Perpetuum/Services/Seasons/SeasonService.cs` | Add `_lastNotifiedSeasonId` field; add `NotifyOnlinePlayersSeasonStarted()` helper; call it from `RefreshCache()` on season ID transition; simplify `SendActivationMailToOnlineCharacters` body | + +No interface changes (`ISeasonService` unchanged). No bootstrapper changes. + +## Success Criteria + +- Online player receives intro email immediately when `#SeasonActivate` is issued (admin command triggers `RefreshCache()` synchronously); or within ≤5 min if the season is activated directly in the DB (next periodic cache refresh). +- No duplicate emails if `RefreshCache()` is called multiple times for the same season. +- Players who log in after season start still receive the email via the existing `OnCharacterLogin` path (unchanged). +- Build compiles with zero errors. diff --git a/docs/superpowers/specs/2026-05-10-seasons-design.md b/docs/superpowers/specs/2026-05-10-seasons-design.md index 1a83135..d92968c 100644 --- a/docs/superpowers/specs/2026-05-10-seasons-design.md +++ b/docs/superpowers/specs/2026-05-10-seasons-design.md @@ -1,7 +1,7 @@ # Seasons System Design **Date:** 2026-05-10 -**Status:** Approved +**Status:** Implemented **Approach:** New Season Service + Mail Feedback (Approach B) --- diff --git a/src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs b/src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs new file mode 100644 index 0000000..5d72e03 --- /dev/null +++ b/src/Perpetuum.Bootstrapper/Modules/SeasonModule.cs @@ -0,0 +1,26 @@ +using Autofac; +using Perpetuum.Services.Seasons; +using Perpetuum.Threading.Process; +using System; + +namespace Perpetuum.Bootstrapper.Modules +{ + internal class SeasonModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().SingleInstance(); + + builder.RegisterType() + .As() + .AutoActivate() + .OnActivated(e => + { + SeasonServiceLocator.Instance = e.Instance; + var pm = e.Context.Resolve(); + pm.AddProcess(e.Instance.ToAsync().AsTimed(TimeSpan.FromMinutes(1))); + }) + .SingleInstance(); + } + } +} diff --git a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs index 8717ade..7b93c44 100644 --- a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs +++ b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs @@ -361,6 +361,7 @@ private void InitContainer(string gameRoot) _builder.RegisterModule(new IntrusionsModule()); _builder.RegisterModule(new ZonesModule()); _builder.RegisterModule(new PbsModule()); + _builder.RegisterModule(new SeasonModule()); _ = _builder.Register>(x => { diff --git a/src/Perpetuum/Accounting/AccountManager.cs b/src/Perpetuum/Accounting/AccountManager.cs index af4b8fd..c5f01c0 100644 --- a/src/Perpetuum/Accounting/AccountManager.cs +++ b/src/Perpetuum/Accounting/AccountManager.cs @@ -1,6 +1,7 @@ using Microsoft.Data.SqlClient; using Perpetuum.Accounting.Characters; using Perpetuum.Data; +using Perpetuum.Services.Seasons; using Perpetuum.Log; using Perpetuum.Services.EventServices; using Perpetuum.Services.ExtensionService; @@ -312,6 +313,8 @@ public void AddExtensionPointsSpent(Account account, Character character, int sp .SetParameter("@extensionLevel", extensionLevel) .SetParameter("@characterID", character.Id) .ExecuteNonQuery().ThrowIfEqual(0, ErrorCodes.SQLInsertError); + + SeasonServiceLocator.Instance?.RecordActivity(character.Id, SeasonActivityType.EpSpent, spentPoints); } /// diff --git a/src/Perpetuum/Accounting/Characters/CharacterWallet.cs b/src/Perpetuum/Accounting/Characters/CharacterWallet.cs index c34bd24..c02e27e 100644 --- a/src/Perpetuum/Accounting/Characters/CharacterWallet.cs +++ b/src/Perpetuum/Accounting/Characters/CharacterWallet.cs @@ -1,5 +1,7 @@ using Perpetuum.Common.Loggers.Transaction; +using Perpetuum.Services.Seasons; using Perpetuum.Wallets; +using System; using System.Collections.Generic; namespace Perpetuum.Accounting.Characters @@ -49,6 +51,11 @@ protected override void OnCommited(double startBalance) .WithData(info) .ToCharacter(character) .Send(); + + 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)); } } } \ No newline at end of file diff --git a/src/Perpetuum/Modules/DrillerModule.cs b/src/Perpetuum/Modules/DrillerModule.cs index c0216cb..f0e555d 100644 --- a/src/Perpetuum/Modules/DrillerModule.cs +++ b/src/Perpetuum/Modules/DrillerModule.cs @@ -1,4 +1,5 @@ using Perpetuum.Data; +using Perpetuum.Services.Seasons; using Perpetuum.EntityFramework; using Perpetuum.ExportedTypes; using Perpetuum.Items; @@ -189,6 +190,7 @@ remoteControlledCreature.CommandRobot is Player ownerPlayer drilledQuantity, terrainLock.Location)); player.Zone?.MiningLogHandler.EnqueueMiningLog(drilledMineralDefinition, drilledQuantity); + SeasonServiceLocator.Instance?.RecordActivity(player.Character.Id, SeasonActivityType.MineralMined, drilledQuantity); resourceStats.Add((material.EntityDefault.Name, material.Quantity)); } diff --git a/src/Perpetuum/Modules/LargeDrillerModule.cs b/src/Perpetuum/Modules/LargeDrillerModule.cs index 9650299..41b6fe4 100644 --- a/src/Perpetuum/Modules/LargeDrillerModule.cs +++ b/src/Perpetuum/Modules/LargeDrillerModule.cs @@ -1,4 +1,5 @@ using Perpetuum.Data; +using Perpetuum.Services.Seasons; using Perpetuum.EntityFramework; using Perpetuum.ExportedTypes; using Perpetuum.Items; @@ -110,6 +111,7 @@ public override void DoExtractMinerals(IZone zone) drilledQuantity, position)); player.Zone?.MiningLogHandler.EnqueueMiningLog(drilledMineralDefinition, drilledQuantity); + SeasonServiceLocator.Instance?.RecordActivity(player.Character.Id, SeasonActivityType.MineralMined, drilledQuantity); resourceStats.Add((material.EntityDefault.Name, material.Quantity)); } diff --git a/src/Perpetuum/Players/Player.cs b/src/Perpetuum/Players/Player.cs index adcaf60..303d48a 100644 --- a/src/Perpetuum/Players/Player.cs +++ b/src/Perpetuum/Players/Player.cs @@ -14,6 +14,7 @@ using Perpetuum.Modules; using Perpetuum.Robots; using Perpetuum.Services.ExtensionService; +using Perpetuum.Services.Seasons; using Perpetuum.Services.Looting; using Perpetuum.Services.MissionEngine; using Perpetuum.Services.MissionEngine.MissionTargets; @@ -1087,6 +1088,9 @@ private void HandlePlayerDead(IZone zone, Unit killer) SaveCombatLog(zone, killer); + if (killer is Player killerPlayer && killerPlayer != this) + SeasonServiceLocator.Instance?.RecordActivity(killerPlayer.Character.Id, SeasonActivityType.PvpKill, 1); + Character character = Character; DockingBase dockingBase = character.GetHomeBaseOrCurrentBase(); diff --git a/src/Perpetuum/Services/Channels/ChatCommands/GameAdminCommands.cs b/src/Perpetuum/Services/Channels/ChatCommands/GameAdminCommands.cs index 19083b0..bd7998f 100644 --- a/src/Perpetuum/Services/Channels/ChatCommands/GameAdminCommands.cs +++ b/src/Perpetuum/Services/Channels/ChatCommands/GameAdminCommands.cs @@ -12,7 +12,9 @@ public class AdminCommandRouter(GlobalConfiguration configuration, ISessionManag private readonly GlobalConfiguration _config = configuration; private readonly ISessionManager _sessionManager = sessionManager; - private readonly Dictionary _commands = typeof(AdminCommandHandlers).GetMethods() + private readonly Dictionary _commands = + new[] { typeof(AdminCommandHandlers), typeof(SeasonAdminCommandHandlers) } + .SelectMany(t => t.GetMethods()) .Where(m => m.GetCustomAttributes(typeof(ChatCommand), false).Length > 0) .Select(m => new KeyValuePair( ((ChatCommand)m.GetCustomAttribute(typeof(ChatCommand))).Command, diff --git a/src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs b/src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs new file mode 100644 index 0000000..4349403 --- /dev/null +++ b/src/Perpetuum/Services/Channels/ChatCommands/SeasonAdminCommandHandlers.cs @@ -0,0 +1,147 @@ +using Perpetuum.Data; +using Perpetuum.Services.Seasons; +using System; +using System.Globalization; + +namespace Perpetuum.Services.Channels.ChatCommands +{ + public static class SeasonAdminCommandHandlers + { + // #SeasonCreate,,, + [ChatCommand("SeasonCreate")] + public static void SeasonCreate(AdminCommandData data) + { + if (data.Command.Args.Length < 3) { SendMessageToAll(data, "Usage: #SeasonCreate,,,"); return; } + 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, "Date format must be YYYY-MM-DD"); return; } + + int id = new SeasonRepository().CreateSeason(name, "", start, end); + SendMessageToAll(data, $"Season created. ID={id} Name='{name}' {start:yyyy-MM-dd} to {end:yyyy-MM-dd}"); + } + + // #SeasonActivate, + [ChatCommand("SeasonActivate")] + public static void SeasonActivate(AdminCommandData data) + { + AdminCommandHandlers.CheckRequiredArgLength(data, 1); + if (!int.TryParse(data.Command.Args[0], out int id)) { SendMessageToAll(data, "Invalid seasonId"); return; } + var repo = new SeasonRepository(); + repo.SetSeasonActive(id, true); + var season = repo.GetSeasonById(id); + if (season != null && SeasonServiceLocator.Instance is SeasonService svc) + svc.SendActivationMailToOnlineCharacters(season); + SendMessageToAll(data, $"Season {id} activated."); + } + + // #SeasonDeactivate, + [ChatCommand("SeasonDeactivate")] + public static void SeasonDeactivate(AdminCommandData data) + { + AdminCommandHandlers.CheckRequiredArgLength(data, 1); + if (!int.TryParse(data.Command.Args[0], out int id)) { SendMessageToAll(data, "Invalid seasonId"); return; } + new SeasonRepository().SetSeasonActive(id, false); + SendMessageToAll(data, $"Season {id} deactivated."); + } + + // #SeasonAddRate,,,, + [ChatCommand("SeasonAddRate")] + public static void SeasonAddRate(AdminCommandData data) + { + if (data.Command.Args.Length < 4) { SendMessageToAll(data, "Usage: #SeasonAddRate,,,,"); return; } + if (!int.TryParse(data.Command.Args[0], out int sid) || + !int.TryParse(data.Command.Args[1], out int act) || + !double.TryParse(data.Command.Args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double pts) || + !int.TryParse(data.Command.Args[3], out int scale)) + { SendMessageToAll(data, "Invalid arguments"); return; } + new SeasonRepository().AddActivityRate(sid, (SeasonActivityType)act, pts, scale); + SendMessageToAll(data, $"Rate added: season={sid} type={act} pts={pts} scale={scale}"); + } + + // #SeasonAddObjective,,,,, + [ChatCommand("SeasonAddObjective")] + public static void SeasonAddObjective(AdminCommandData data) + { + if (data.Command.Args.Length < 5) { SendMessageToAll(data, "Usage: #SeasonAddObjective,,,,,"); return; } + if (!int.TryParse(data.Command.Args[0], out int sid) || + !int.TryParse(data.Command.Args[1], out int act) || + !long.TryParse(data.Command.Args[2], out long target) || + !int.TryParse(data.Command.Args[3], out int bonus)) + { SendMessageToAll(data, "Invalid arguments"); return; } + string name = data.Command.Args[4]; + new SeasonRepository().AddObjective(sid, (SeasonActivityType)act, target, bonus, name, ""); + SendMessageToAll(data, $"Objective '{name}' added to season {sid}"); + } + + // #SeasonAddTier,,,,, + [ChatCommand("SeasonAddTier")] + public static void SeasonAddTier(AdminCommandData data) + { + if (data.Command.Args.Length < 5) { SendMessageToAll(data, "Usage: #SeasonAddTier,,,,,"); return; } + if (!int.TryParse(data.Command.Args[0], out int sid) || + !int.TryParse(data.Command.Args[1], out int num) || + !int.TryParse(data.Command.Args[3], out int pts) || + !int.TryParse(data.Command.Args[4], out int pkg)) + { SendMessageToAll(data, "Invalid arguments"); return; } + new SeasonRepository().AddTier(sid, num, data.Command.Args[2], pts, pkg); + SendMessageToAll(data, $"Tier '{data.Command.Args[2]}' added to season {sid}"); + } + + // #SeasonAddLeaderboard,,,, + [ChatCommand("SeasonAddLeaderboard")] + public static void SeasonAddLeaderboard(AdminCommandData data) + { + if (data.Command.Args.Length < 4) { SendMessageToAll(data, "Usage: #SeasonAddLeaderboard,,,,"); return; } + if (!int.TryParse(data.Command.Args[0], out int sid) || + !int.TryParse(data.Command.Args[1], out int rmin) || + !int.TryParse(data.Command.Args[2], out int rmax) || + !int.TryParse(data.Command.Args[3], out int pkg)) + { SendMessageToAll(data, "Invalid arguments"); return; } + new SeasonRepository().AddLeaderboardReward(sid, rmin, rmax, pkg); + SendMessageToAll(data, $"Leaderboard reward added: season={sid} ranks={rmin}-{rmax} pkg={pkg}"); + } + + // #SeasonStatus + [ChatCommand("SeasonStatus")] + public static void SeasonStatus(AdminCommandData data) + { + var (name, remaining, count) = new SeasonRepository().GetSeasonStatus(); + if (name == "(none)") { SendMessageToAll(data, "No active season."); return; } + string t = remaining > TimeSpan.Zero ? $"{(int)remaining.TotalHours}h {remaining.Minutes}m remaining" : "ENDED"; + SendMessageToAll(data, $"Season: '{name}' | {t} | {count} participants"); + } + + // #SeasonInfo, + [ChatCommand("SeasonInfo")] + public static void SeasonInfo(AdminCommandData data) + { + AdminCommandHandlers.CheckRequiredArgLength(data, 1); + if (!int.TryParse(data.Command.Args[0], out int id)) { SendMessageToAll(data, "Invalid seasonId"); return; } + var repo = new SeasonRepository(); + var season = repo.GetSeasonById(id); + if (season == null) { SendMessageToAll(data, $"Season {id} not found."); return; } + var rates = repo.GetActivityRates(id); + var tiers = repo.GetTiers(id); + var objs = repo.GetObjectives(id); + var lb = repo.GetLeaderboardRewards(id); + SendMessageToAll(data, $"Season {id}: '{season.Name}' active={season.IsActive} {season.StartTime:yyyy-MM-dd} to {season.EndTime:yyyy-MM-dd} | Rates:{rates.Count} Tiers:{tiers.Count} Objectives:{objs.Count} LB:{lb.Count}"); + } + + // #SeasonForceEnd, + [ChatCommand("SeasonForceEnd")] + public static void SeasonForceEnd(AdminCommandData data) + { + AdminCommandHandlers.CheckRequiredArgLength(data, 1); + if (!int.TryParse(data.Command.Args[0], out int id)) { SendMessageToAll(data, "Invalid seasonId"); return; } + Db.Query("UPDATE seasons SET end_time = DATEADD(MINUTE, -1, GETUTCDATE()) WHERE id = @id") + .SetParameter("@id", id).ExecuteNonQuery(); + SendMessageToAll(data, $"Season {id} end_time set to past. Processing within 1 minute."); + } + + private static void SendMessageToAll(AdminCommandData data, string message) + { + data.Channel.SendMessageToAll(data.SessionManager, data.Sender, message); + } + } +} diff --git a/src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs b/src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs index 51fc58b..d00994d 100644 --- a/src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs +++ b/src/Perpetuum/Services/MissionEngine/MissionProcessorObjects/MissionProcessorAdvanceTarget.cs @@ -1,5 +1,6 @@ using Perpetuum.Accounting.Characters; using Perpetuum.Data; +using Perpetuum.Services.Seasons; using Perpetuum.Items; using Perpetuum.Log; using Perpetuum.Services.MissionEngine.AdministratorObjects; @@ -550,6 +551,12 @@ private IList ProcessOneAdvancedTarget(Character character, MissionTargetI //write mission log - mission completed missionInProgress.SetSuccessToMissionLog(true).ThrowIfError(); + foreach (Character? p in participants) + { + if (p != null) + SeasonServiceLocator.Instance?.RecordActivity(p.Id, SeasonActivityType.MissionComplete, 1); + } + AdvanceBonusInGang(participants, missionInProgress); //remove mission diff --git a/src/Perpetuum/Services/Seasons/ISeasonService.cs b/src/Perpetuum/Services/Seasons/ISeasonService.cs new file mode 100644 index 0000000..d419199 --- /dev/null +++ b/src/Perpetuum/Services/Seasons/ISeasonService.cs @@ -0,0 +1,10 @@ +using Perpetuum.Accounting.Characters; + +namespace Perpetuum.Services.Seasons +{ + public interface ISeasonService + { + void RecordActivity(int characterId, SeasonActivityType type, long amount); + void OnCharacterLogin(Character character); + } +} diff --git a/src/Perpetuum/Services/Seasons/SeasonActivityType.cs b/src/Perpetuum/Services/Seasons/SeasonActivityType.cs new file mode 100644 index 0000000..917e24c --- /dev/null +++ b/src/Perpetuum/Services/Seasons/SeasonActivityType.cs @@ -0,0 +1,14 @@ +namespace Perpetuum.Services.Seasons +{ + public enum SeasonActivityType + { + NpcKill = 1, + PvpKill = 2, + MissionComplete = 3, + MineralMined = 4, + EpSpent = 5, + NicEarned = 6, + NicSpent = 7, + IntrusionPoint = 8, + } +} diff --git a/src/Perpetuum/Services/Seasons/SeasonModels.cs b/src/Perpetuum/Services/Seasons/SeasonModels.cs new file mode 100644 index 0000000..eaef1bf --- /dev/null +++ b/src/Perpetuum/Services/Seasons/SeasonModels.cs @@ -0,0 +1,70 @@ +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; } + } +} diff --git a/src/Perpetuum/Services/Seasons/SeasonRepository.cs b/src/Perpetuum/Services/Seasons/SeasonRepository.cs new file mode 100644 index 0000000..88567a3 --- /dev/null +++ b/src/Perpetuum/Services/Seasons/SeasonRepository.cs @@ -0,0 +1,436 @@ +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("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 = DateTime.SpecifyKind(record.GetValue("start_time"), DateTimeKind.Utc), + EndTime = DateTime.SpecifyKind(record.GetValue("end_time"), DateTimeKind.Utc), + IsActive = record.GetValue("is_active"), + }; + } + + public List GetActivityRates(int seasonId) + { + return Db.Query("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("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("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("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 upserts points and returns the new running total. + public long AddPoints(int characterId, int seasonId, long points) + { + Db.Query(@" + 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);") + .SetParameter("@characterId", characterId) + .SetParameter("@seasonId", seasonId) + .SetParameter("@points", points) + .ExecuteNonQuery(); + + return Db.Query("SELECT total_points FROM season_character_points " + + "WHERE character_id = @characterId AND season_id = @seasonId") + .SetParameter("@characterId", characterId) + .SetParameter("@seasonId", seasonId) + .ExecuteScalar(); + } + + // ── Objective progress ─────────────────────────────────────────────── + + /// + /// Increments objective progress if not yet completed. + /// Returns (currentValue, bonusAwarded). + /// + public (long currentValue, bool bonusAwarded) IncrementObjectiveProgress( + int characterId, int seasonId, int objectiveId, long amount) + { + Db.Query(@" + 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);") + .SetParameter("@characterId", characterId) + .SetParameter("@seasonId", seasonId) + .SetParameter("@objectiveId", objectiveId) + .SetParameter("@amount", amount) + .ExecuteNonQuery(); + + var record = Db.Query("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) + .ExecuteSingleRow(); + + return (record.GetValue("current_value"), + record.GetValue("bonus_awarded")); + } + + /// + /// Marks objective bonus as awarded. Returns true if this call was first. + /// + public bool MarkObjectiveBonusAwarded(int characterId, int seasonId, int objectiveId) + { + int rows = Db.Query("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("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 guard. Returns true if newly inserted. + public bool InsertTierClaim(int characterId, int seasonId, int tierId) + { + int rows = Db.Query(@" + 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("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("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("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("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("UPDATE seasons SET is_active = 0 WHERE id = @id") + .SetParameter("@id", seasonId) + .ExecuteNonQuery(); + } + + // ── Intro mail tracking ────────────────────────────────────────────── + + /// + /// Ensures a row exists for this character+season and marks intro mail sent. + /// Returns true if the mail had not been sent before. + /// + public bool TryMarkIntroMailSent(int characterId, int seasonId) + { + // Ensure row exists + Db.Query(@" + 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("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("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("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("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("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("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("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("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 = DateTime.SpecifyKind(record.GetValue("end_time"), DateTimeKind.Utc); + return (record.GetValue("name"), + endTime - DateTime.UtcNow, + record.GetValue("cnt")); + } + + public Season? GetSeasonById(int seasonId) + { + var record = Db.Query("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 = DateTime.SpecifyKind(record.GetValue("start_time"), DateTimeKind.Utc), + EndTime = DateTime.SpecifyKind(record.GetValue("end_time"), DateTimeKind.Utc), + IsActive = record.GetValue("is_active"), + }; + } + } +} diff --git a/src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs b/src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs new file mode 100644 index 0000000..f8e20cf --- /dev/null +++ b/src/Perpetuum/Services/Seasons/SeasonServiceLocator.cs @@ -0,0 +1,7 @@ +namespace Perpetuum.Services.Seasons +{ + public static class SeasonServiceLocator + { + public static ISeasonService? Instance { get; set; } + } +} diff --git a/src/Perpetuum/Zones/Intrusion/Outpost.cs b/src/Perpetuum/Zones/Intrusion/Outpost.cs index 36b8af4..4e515a6 100644 --- a/src/Perpetuum/Zones/Intrusion/Outpost.cs +++ b/src/Perpetuum/Zones/Intrusion/Outpost.cs @@ -1,4 +1,5 @@ using Perpetuum.Accounting.Characters; +using Perpetuum.Services.Seasons; using Perpetuum.Common; using Perpetuum.Common.Loggers.Transaction; using Perpetuum.Data; @@ -551,6 +552,7 @@ private void ProcessStabilityChange(StabilityAffectingEvent sap) foreach (Players.Player player in sap.Participants) { player.Character.AddExtensionPointsBoostAndLog(EpForActivityType.Intrusion, EP_WINNER); + SeasonServiceLocator.Instance?.RecordActivity(player.Character.Id, SeasonActivityType.IntrusionPoint, 1); } //make dem toast anyways diff --git a/src/Perpetuum/Zones/NpcSystem/Npc.cs b/src/Perpetuum/Zones/NpcSystem/Npc.cs index cbaa44b..76babca 100644 --- a/src/Perpetuum/Zones/NpcSystem/Npc.cs +++ b/src/Perpetuum/Zones/NpcSystem/Npc.cs @@ -1,5 +1,6 @@ using Perpetuum.Data; using Perpetuum.EntityFramework; +using Perpetuum.Services.Seasons; using Perpetuum.Log; using Perpetuum.Players; using Perpetuum.Services.Looting; @@ -255,6 +256,9 @@ private void HandleNpcDead([NotNull] IZone zone, Unit killer, Player tagger) Player killerPlayer = zone.ToPlayerOrGetOwnerPlayer(killer); + if (killerPlayer != null) + SeasonServiceLocator.Instance?.RecordActivity(killerPlayer.Character.Id, SeasonActivityType.NpcKill, 1); + if (GetMissionGuid() != Guid.Empty) { Logger.DebugInfo(" >>>> NPC is mission related."); From af4852de1d1dd96b4a562b2d62328268bc754225 Mon Sep 17 00:00:00 2001 From: "Dolzhukov, Viktor" Date: Sun, 10 May 2026 20:19:18 +0500 Subject: [PATCH 05/11] docs(seasons): add Seasons Admin Tool design spec --- .../2026-05-10-seasons-admin-tool-design.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-10-seasons-admin-tool-design.md diff --git a/docs/superpowers/specs/2026-05-10-seasons-admin-tool-design.md b/docs/superpowers/specs/2026-05-10-seasons-admin-tool-design.md new file mode 100644 index 0000000..2afda49 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-seasons-admin-tool-design.md @@ -0,0 +1,292 @@ +# Seasons Admin Tool Design + +**Date:** 2026-05-10 +**Status:** Approved +**Scope:** New "Seasons" tab in `Perpetuum.AdminTool` — season management, packages management, and per-season statistics. + +--- + +## Overview + +The Seasons admin tab allows server administrators to create and manage game seasons, configure reward packages, and view per-season statistics. It integrates with the existing change-queue commit pattern used throughout the Admin Tool. + +The tab has two top-level views switched by a segmented control in the tab header: +- **Seasons** — dashboard of season cards + creation wizard +- **Packages** — master-detail manager for reward packages and their items + +--- + +## Constraints & Patterns + +- Follows the existing `Perpetuum.AdminTool` patterns exactly: + - MVVM with CommunityToolkit.Mvvm + - Repository classes for SQL access (direct `Microsoft.Data.SqlClient`) + - `ChangeQueue` / `IPendingChange` for deferred, reviewable edits + - WPF DataGrids with inline editing for list data + - Modal dialogs for add/edit of individual rows where inline editing is insufficient +- Packages are scoped to the Seasons tab for now; the implementation should not assume they are seasons-exclusive (easy to extract later). +- No server-side API calls — the tool reads and writes directly to the SQL Server database. + +--- + +## Data Access + +The following tables are read and written. Schema is defined in the Seasons design spec (`2026-05-10-seasons-design.md`). + +| Table | Operations | +|---|---| +| `seasons` | Select all, insert, update (name, description, dates, is_active) | +| `season_activity_rates` | Select by season_id, insert, update, delete | +| `season_objectives` | Select by season_id, insert, update, delete | +| `season_tiers` | Select by season_id, insert, update, delete | +| `season_leaderboard_rewards` | Select by season_id, insert, update, delete | +| `packages` | Select all (for dropdowns and Packages view) | +| `packageitems` | Select by package_id, insert, delete | +| `entitydefaults` | Read via `LookupCache` (adds `hidden` column to existing query — additive only) | +| `season_character_points` | Select (statistics only — never written by admin tool) | +| `season_objective_progress` | Select (statistics only — never written by admin tool) | +| `season_tier_claims` | Select (statistics only — never written by admin tool) | + +--- + +## Tab Structure + +### Seasons View (default) + +**Header bar:** +- Segmented switcher: `[Seasons] [Packages]` — Seasons selected by default +- `+ New Season` button — opens the creation wizard + +**Content: Season Cards** + +One card per season, laid out in a wrapping grid. Three visual states: + +| State | Border | Condition | +|---|---|---| +| Active | Blue (2px solid) | `is_active = 1` | +| Draft | Dashed grey | `is_active = 0` and `end_time` in the future | +| Ended | Solid grey, dimmed | `is_active = 0` and `end_time` in the past | + +Each card shows: status badge, season name, date range, time remaining (Active) or end date (others), participant count (Active/Ended), configured tier count, and a `Manage →` / `Edit →` / `View →` button that drills into the season detail view. + +A `+ New Season` placeholder card at the end of the grid also opens the wizard. + +--- + +### Season Detail View + +Replaces the cards view (full-area navigation, not a modal). A back arrow `← All Seasons` returns to the cards. + +**Header bar:** Season name, status badge, Activate / Deactivate buttons (with confirmation dialog). Both buttons are always visible; the irrelevant one is disabled. + +**Tab bar:** General · Activity Rates · Objectives · Tiers · Leaderboard · Packages · 📊 Statistics + +#### General Tab +Fields: Name, Description, Start Time, End Time, Season ID (read-only). All fields editable; changes go to the change queue. + +#### Activity Rates Tab +All 8 `SeasonActivityType` values are pre-listed as fixed rows (no add/delete — the set of activity types is defined in code). If no `season_activity_rates` row exists in the database yet for a given type, the row renders with default values (pts/unit = 0, scale = 1). Saving queues an upsert (insert if missing, update if present). Each row has: +- Activity type label (human-readable name) +- `Points per Unit` — editable numeric field +- `Scale` — editable numeric field +- `Effective rate` — computed label shown inline (e.g., "10 pts per kill", "1 pt per 1,000 units mined"). Set Points per Unit to 0 to disable an activity type for this season. + +#### Objectives Tab +DataGrid with columns: Name, Description, Activity Type (dropdown), Target Value, Bonus Points, Display Order. +- `+ Add Objective` button opens a modal add/edit dialog. +- Inline edit on existing rows; delete with confirmation. +- Changes go to the change queue. + +#### Tiers Tab +DataGrid with columns: Tier #, Tier Name, Points Required, Reward Package (dropdown of all packages). +- `+ Add Tier` button adds a new inline row. +- Rows are ordered by Tier # ascending; validation warns if points are not strictly ascending. +- Delete with confirmation; changes go to the change queue. + +#### Leaderboard Tab +DataGrid with columns: Rank Min, Rank Max, Reward Package (dropdown). +- Validation: rank ranges must not overlap. Gap warning shown if consecutive brackets are not contiguous (e.g., rank 4 is not covered). +- `+ Add Bracket` button adds a new inline row. +- Changes go to the change queue. + +#### Packages Tab (within detail) +Same content as the top-level Packages view (see below), but with packages that are referenced by this season's tiers or leaderboard rewards highlighted. Provides convenient access without leaving the season detail. + +#### Statistics Tab +See the Statistics section below. + +--- + +### Packages View + +Activated by the `Packages` segment in the tab header. Uses a master-detail layout: + +**Left panel — Package List** +- Filter input at top (filters by package name, live) +- Each list item shows: package name, item count, "Used by N seasons" or "Not used" subtitle +- Unused packages are visually dimmed +- `+ New Package` button in the header creates a new package (name prompt dialog → adds to list and selects it) + +**Right panel — Package Detail** +- Package name (editable) +- Usage line: lists every season and context (tier/leaderboard) that references this package +- Warning banner if the package is referenced by an active season: "Changes will affect players who have not yet claimed this reward." +- Items DataGrid: Item display name (resolved — see Entity Picker below), Quantity (editable), delete button per row +- `+ Add Item` button opens entity picker dialog (see Entity Picker below) +- `Delete Package` button (disabled if the package is referenced by any season — active or ended — since ended seasons may still have unclaimed rewards; tooltip explains why) +- All changes go to the change queue + +--- + +## Creation Wizard + +Opened by `+ New Season` button or the placeholder card. A modal dialog with a 6-step progress indicator at the top. Back is always available; Next validates the current step before advancing. + +| Step | Title | Content | Optional | +|---|---|---|---| +| 1 | Season Info | Name, Description, Start Time, End Time. Validates end > start. | No | +| 2 | Activity Rates | Same grid as the Activity Rates tab — all 8 types pre-listed with pts/unit, scale, and live effective rate label. | No (but all rates may be 0) | +| 3 | Objectives | Add milestone objectives via inline rows. Name, activity type, target value, bonus points. | Yes | +| 4 | Tiers | Add tiers with name, point threshold, and package dropdown. If no packages exist, a warning with a link-style hint to create packages first. | Yes | +| 5 | Leaderboard Rewards | Add rank brackets with package dropdown. Overlap and gap validation same as detail tab. | Yes | +| 6 | Review | Read-only summary of all configuration. "Add to Change Queue" button. | No | + +On completing Step 6, all rows are added to the `ChangeQueue` as `IPendingChange` objects. The season is created with `is_active = 0` (Draft). The admin activates it separately via the General tab's Activate button after committing the change queue. + +Each step has a descriptive help block explaining what the step configures and defining any non-obvious terms (e.g., Scale, Points per Unit). + +--- + +## Statistics Tab + +Available inside every season's detail view (active and ended). Divided into two sections. + +### Participation Health +| Metric | Source | +|---|---| +| Total Participants | COUNT(*) from `season_character_points` | +| Active Last 7 Days | COUNT where `last_updated` ≥ now − 7d | +| Time Remaining / Season Status | Computed from `end_time` | +| Retention Rate | Active 7d / Total × 100% | +| Tier Distribution | COUNT per tier from `season_tier_claims` grouped by tier_id | +| Top 10 Leaderboard | `season_character_points` ORDER BY total_points DESC TOP 10, joined to `characters` table for display name | + +### Balance Tuning +| Metric | Source | +|---|---| +| Points by Activity Type | Requires per-activity breakout — see note below | +| Avg Points per Day | Total points / elapsed season days, shown as a per-day bar chart (one bar per elapsed day) | +| Objective Completion Rates | `season_objective_progress.completed` count / total participants per objective | +| Balance Insight | Computed text: projects days-to-tier for a new player at current avg velocity; flags activity types with < 15% participation | + +**Note on activity breakdown:** The current schema (`season_character_points`) stores only a cumulative total, not a per-activity breakdown. Showing points broken out by activity type requires a future schema addition (additional tracking columns or a separate summary table). For this phase, the "Points by Activity Type" section renders a static notice explaining the limitation. The section is reserved in the layout so it can be filled in when the schema supports it. + +Statistics are **read-only** and loaded on tab activation (not live-updating). A `Refresh` button re-runs the queries. + +--- + +## Entity Picker for Package Items + +### Caching + +The entity picker reuses `LookupCache.Entities`, which is already loaded on app start and refreshed after every successful commit. No additional database query is needed at pick time. `LookupCache.RefreshEntitiesAsync` reads `definition`, `definitionname`, `categoryflags`, and `enabled` from `entitydefaults`. + +### Filtering + +A `PackageItemPickerViewModel` (or equivalent filtered collection) applies three filters to `LookupCache.Entities` to produce the allowed item set: + +1. **Enabled:** `EntityPickItem.Enabled == true` (NULL in DB → true, so LookupCache already handles this correctly) +2. **Not hidden:** requires adding `hidden` to `LookupCache`'s query (currently not loaded). Load it as `hidden` (bit, nullable, NULL → false). Exclude items where `hidden == true`. +3. **Category match:** item's `CategoryFlags` must fall within one of the following root categories **or any of their descendant categories**: + + | Category flag name | + |---| + | `cf_robots` | + | `cf_ammo` | + | `cf_robot_equipment` | + | `cf_material` | + | `cf_production_items` | + | `cf_gift_packages` | + | `cf_consumable_items` | + | `cf_consumable_boosters` | + | `cf_field_accessories` | + | `cf_pbs_capsules` | + | `cf_redeemables` | + + Category hierarchy matching follows the existing bitwise mask pattern used in `RobotTemplateSlotViewModel.RebuildAmmoPicks()`: for each allowed root flag value `target`, compute `mask = CategoryFlagsMask(target)` and accept an entity if `(entity.CategoryFlags & mask) == target`. An entity is accepted if it matches **any** of the 11 roots. + + Build the filtered collection once on load and on `LookupCache` refresh; do not recompute per-keystroke. + +### Search + +The entity picker dialog presents a searchable ComboBox (`IsEditable="True"`, `IsTextSearchEnabled="True"`) bound to the pre-filtered collection. Search matches against the display name. + +### Display Names + +Entity display names are resolved in priority order: + +1. If `EntityDefaultRow.DescriptionToken` is non-null, look up the token in `TranslationStore` for the active language (langId 1 = English as used elsewhere in the tool). Use the resolved string if present and non-empty. +2. Fall back to `EntityDefaultRow.DefinitionName` (always present). + +The resolved name is used both in the picker dropdown and in the package items DataGrid (`Display` column). The `definition` integer (ID) is stored in `packageitems`; the name is display-only and re-resolved from cache on load. + +A `PackageItemPickItem` record wraps the resolved display name and definition ID, following the `EntityPickItem` pattern: +```csharp +public record PackageItemPickItem(int Definition, string DisplayName) +{ + public string Display => $"{Definition} — {DisplayName}"; +} +``` + +### LookupCache change required + +Add `hidden` to the `LookupCache` entity query and expose it on `EntityPickItem`: +```sql +-- existing: +SELECT definition, definitionname, categoryflags, enabled FROM entitydefaults +-- updated: +SELECT definition, definitionname, categoryflags, enabled, hidden FROM entitydefaults +``` + +This is a small, additive change to `LookupCache` and `EntityPickItem`. No existing consumers are affected since they currently don't reference `hidden`. + +--- + +## Project Layout + +Following existing Admin Tool patterns: + +| File | Role | +|---|---| +| `Views/SeasonsView.xaml` + `.cs` | Seasons/Packages switcher, season cards, drill-down host | +| `Views/SeasonDetailView.xaml` + `.cs` | Tabbed detail view for one season | +| `Views/SeasonWizardWindow.xaml` + `.cs` | 6-step creation wizard dialog | +| `Views/PackagesView.xaml` + `.cs` | Packages master-detail | +| `ViewModels/SeasonsViewModel.cs` | Seasons cards list, navigation state, wizard trigger | +| `ViewModels/SeasonDetailViewModel.cs` | Selected season, tab state, activate/deactivate | +| `ViewModels/SeasonWizardViewModel.cs` | Wizard step state, per-step view models | +| `ViewModels/SeasonStatisticsViewModel.cs` | Statistics queries and computed metrics | +| `ViewModels/PackagesViewModel.cs` | Package list, selected package, item editing | +| `Seasons/SeasonRepository.cs` | All SQL reads for seasons and statistics | +| `Seasons/SeasonRow.cs` | Row model for `seasons` table | +| `Seasons/SeasonObjectiveRow.cs` | Row model for `season_objectives` | +| `Seasons/SeasonTierRow.cs` | Row model for `season_tiers` | +| `Seasons/SeasonLeaderboardRewardRow.cs` | Row model for `season_leaderboard_rewards` | +| `Seasons/SeasonActivityRateRow.cs` | Row model for `season_activity_rates` | +| `Seasons/SeasonChanges.cs` | `IPendingChange` implementations for all season mutations | +| `Packages/PackageRepository.cs` | SQL reads for packages and packageitems | +| `Packages/PackageRow.cs` | Row model for `packages` | +| `Packages/PackageItemRow.cs` | Row model for `packageitems` | +| `Packages/PackageItemPickItem.cs` | Filtered/resolved pick item for entity picker | +| `Packages/PackageChanges.cs` | `IPendingChange` implementations for package mutations | + +The `Seasons` tab is registered in `MainViewModel` alongside existing tabs and wired to a `SeasonsViewModel` in the bootstrapper. + +--- + +## Out of Scope + +- Per-activity-type point breakdown in statistics (schema limitation — reserved placeholder in UI) +- Moving Packages management out of the Seasons tab +- Live-updating statistics dashboard +- Bulk import/export of season configuration From 887f0f0efc9fb036ae965b11265fbc099b1a7e79 Mon Sep 17 00:00:00 2001 From: "Dolzhukov, Viktor" Date: Mon, 11 May 2026 16:24:01 +0500 Subject: [PATCH 06/11] =?UTF-8?q?feat(seasons):=20Seasons=20Admin=20Tool?= =?UTF-8?q?=20=E2=80=94=20smoke-test=20fixes=20and=20improvements=20(Round?= =?UTF-8?q?=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 8 issues and add 5 improvements found during smoke testing: Fixes: - Fix SQL variable collision when queuing multiple packages in one batch (@pkgId_{guid8}) - Wizard Step 3 objective dropdown now filters to active activity types only - Tier/leaderboard package ComboBox shows name after focus loss (SelectedPackage property) - Activity rate Queue Save disabled for new rows with PointsPerUnit=0 - Objectives no longer queue with default values on add; explicit Queue Save per row - Server startup: characters connecting before first RefreshCache tick now receive intro mail - Admin deactivation before season end now triggers ProcessSeasonEnd (rewards + emails) Improvements: - Wizard Finish emits one combined SQL batch (season + rates + objectives + tiers + leaderboard) - Wizard Step 6 shows full structured review (rates, objectives, tiers, leaderboard) - Package item picker displays tier labels (T4P, Mk2, T3+) from entitydefaults.tiertype/tierlevel - Wizard Step 3 shows total objective bonus points available - Wizard Step 4 shows top tier threshold vs. objective bonus available Also fixes gitignore: add exception for AdminTool Packages source directory. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + .../152-1778423504/content/detail-tabs.html | 122 + .../152-1778423504/content/detail-view.html | 75 + .../content/management-approaches.html | 114 + .../152-1778423504/content/packages-view.html | 102 + .../content/statistics-tab.html | 149 + .../152-1778423504/content/tab-overview.html | 75 + .../152-1778423504/content/welcome.html | 5 + .../152-1778423504/content/wizard-flow.html | 149 + .../152-1778423504/state/server-stopped | 1 + .../152-1778423504/state/server.pid | 1 + .../plans/2026-05-10-seasons-admin-tool.md | 3696 +++++++++++++++++ ...6-05-11-seasons-admin-tool-fixes-round2.md | 1040 +++++ ...26-05-11-seasons-admin-tool-smoke-fixes.md | 762 ++++ .../2026-05-10-seasons-admin-tool-design.md | 243 ++ ...6-05-11-seasons-admin-tool-fixes-design.md | 258 ++ src/Perpetuum.AdminTool/App.xaml | 1 + .../Common/EntityPickItem.cs | 10 +- src/Perpetuum.AdminTool/Common/LookupCache.cs | 22 +- .../Packages/PackageChanges.cs | 60 + .../Packages/PackageItemPickItem.cs | 92 + .../Packages/PackageItemRow.cs | 23 + .../Packages/PackageRepository.cs | 111 + .../Packages/PackageRow.cs | 30 + .../Seasons/SeasonActivityRateRow.cs | 78 + .../Seasons/SeasonChanges.cs | 93 + .../Seasons/SeasonLeaderboardRewardRow.cs | 22 + .../Seasons/SeasonObjectiveRow.cs | 19 + .../Seasons/SeasonRepository.cs | 270 ++ src/Perpetuum.AdminTool/Seasons/SeasonRow.cs | 71 + .../Seasons/SeasonTierRow.cs | 23 + .../ViewModels/MainViewModel.cs | 10 + .../ViewModels/PackagesViewModel.cs | 317 ++ .../ViewModels/SeasonDetailViewModel.cs | 390 ++ .../ViewModels/SeasonStatisticsViewModel.cs | 56 + .../ViewModels/SeasonWizardViewModel.cs | 353 ++ .../ViewModels/SeasonsViewModel.cs | 145 + src/Perpetuum.AdminTool/Views/MainWindow.xaml | 3 + .../Views/PackagesView.xaml | 156 + .../Views/PackagesView.xaml.cs | 22 + .../Views/SeasonDetailView.xaml | 347 ++ .../Views/SeasonDetailView.xaml.cs | 28 + .../Views/SeasonWizardWindow.xaml | 395 ++ .../Views/SeasonWizardWindow.xaml.cs | 31 + .../Views/SeasonsView.xaml | 149 + .../Views/SeasonsView.xaml.cs | 34 + .../Services/Seasons/SeasonService.cs | 43 +- 47 files changed, 10175 insertions(+), 22 deletions(-) create mode 100644 .superpowers/brainstorm/152-1778423504/content/detail-tabs.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/detail-view.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/management-approaches.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/packages-view.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/statistics-tab.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/tab-overview.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/welcome.html create mode 100644 .superpowers/brainstorm/152-1778423504/content/wizard-flow.html create mode 100644 .superpowers/brainstorm/152-1778423504/state/server-stopped create mode 100644 .superpowers/brainstorm/152-1778423504/state/server.pid create mode 100644 docs/superpowers/plans/2026-05-10-seasons-admin-tool.md create mode 100644 docs/superpowers/plans/2026-05-11-seasons-admin-tool-fixes-round2.md create mode 100644 docs/superpowers/plans/2026-05-11-seasons-admin-tool-smoke-fixes.md create mode 100644 docs/superpowers/specs/2026-05-11-seasons-admin-tool-fixes-design.md create mode 100644 src/Perpetuum.AdminTool/Packages/PackageChanges.cs create mode 100644 src/Perpetuum.AdminTool/Packages/PackageItemPickItem.cs create mode 100644 src/Perpetuum.AdminTool/Packages/PackageItemRow.cs create mode 100644 src/Perpetuum.AdminTool/Packages/PackageRepository.cs create mode 100644 src/Perpetuum.AdminTool/Packages/PackageRow.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonActivityRateRow.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonChanges.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonLeaderboardRewardRow.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonObjectiveRow.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonRepository.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonRow.cs create mode 100644 src/Perpetuum.AdminTool/Seasons/SeasonTierRow.cs create mode 100644 src/Perpetuum.AdminTool/ViewModels/PackagesViewModel.cs create mode 100644 src/Perpetuum.AdminTool/ViewModels/SeasonDetailViewModel.cs create mode 100644 src/Perpetuum.AdminTool/ViewModels/SeasonStatisticsViewModel.cs create mode 100644 src/Perpetuum.AdminTool/ViewModels/SeasonWizardViewModel.cs create mode 100644 src/Perpetuum.AdminTool/ViewModels/SeasonsViewModel.cs create mode 100644 src/Perpetuum.AdminTool/Views/PackagesView.xaml create mode 100644 src/Perpetuum.AdminTool/Views/PackagesView.xaml.cs create mode 100644 src/Perpetuum.AdminTool/Views/SeasonDetailView.xaml create mode 100644 src/Perpetuum.AdminTool/Views/SeasonDetailView.xaml.cs create mode 100644 src/Perpetuum.AdminTool/Views/SeasonWizardWindow.xaml create mode 100644 src/Perpetuum.AdminTool/Views/SeasonWizardWindow.xaml.cs create mode 100644 src/Perpetuum.AdminTool/Views/SeasonsView.xaml create mode 100644 src/Perpetuum.AdminTool/Views/SeasonsView.xaml.cs diff --git a/.gitignore b/.gitignore index 6afbe7b..31302ae 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/ 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.

+ +
+
Season 3 — Tiers Tab (example)
+
+ + +
+ ← 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 NamePoints RequiredReward PackageActions +
+ +
+ 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.

+
+
Season 3 — Detail View
+
+
+ ← 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.

+
+
Season 3 — Detail View
+
+
+ ← 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 Tab — Master-Detail
+
+
+
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.

+
+
Seasons Tab — Accordion
+
+
+ Season: +
▾ Season 3 (ACTIVE)
+
+ New Season
+
+
+
▶ General Info Season 3 · 2026-03-01 → 2026-05-31
+
+
▼ Activity Rates 8 types configured
+
+ ActivityPts/UnitScale + NPC Kill101 + PvP Kill501 + Mining11000 +
+
+
▶ 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.

+
+
Seasons Tab — Cards Dashboard
+
+
+
+
● ACTIVE
+
Season 3
+
Ends: 2026-05-31
+
1,247 participants
+
Manage →
+
+
+
ENDED
+
Season 2
+
Mar–Apr 2026
+
892 participants
+
View →
+
+
+
+ New Season
+
+
+
+
+
+
+ +
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.

+ +
+
Seasons Tab → Packages View
+
+ + +
+
+
Seasons
+
Packages
+
+
+ New Package
+
+ + +
+ + +
+
+
🔍 Filter packages…
+
+
+ +
+
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)QuantityIs StackableDel +
+
+ 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?

+ +
+
Season 3 — Statistics
+
+ + +
Participation Health
+
+
+
1,247
+
Total Participants
+
+
+
389
+
Active (last 7 days)
+
+
+
18d 4h
+
Time Remaining
+
+
+
31%
+
Retention (7d active / total)
+
+
+ +
+ + +
+
Tier Distribution
+
+
+
Tier 4 · Gold47 players (3.8%)
+
+
+
+
Tier 3 · Silver183 players (14.7%)
+
+
+
+
Tier 2 · Bronze412 players (33%)
+
+
+
+
Tier 1 · Stone605 players (48.5%)
+
+
+
+
+ + +
+
Top 10 Leaderboard
+
+
+ #CharacterPoints +
+
+ 1Aethon Prime48,210 +
+
+ 2Krixath41,780 +
+
+ 3Velindra39,950 +
+
+ 4Solvax31,200 +
+
+ 5–1018k–28k +
+
+
+
+ + +
Balance Tuning
+
+ + +
+
+
Points by Activity Type
+
+ ActivityTotal PtsAvg/Player +
+
+ NPC Kill12.4M9,942 +
+
+ Mining8.1M6,497 +
+
+ NIC Earned5.7M4,571 +
+
+ PvP Kill2.2M1,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 NPCs78% + Mine 10k Ore44% + Win 5 PvP12% +
+
+
+
+ + +
+ ⚡ 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.

+ +
+
MainWindow → Seasons Tab
+
+ + +
+
+
Seasons
+
Packages
+
+
+ New Season
+
+ + +
+ + +
+
● 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
+
+
2 tiers set up
+
+
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.

+ +
+
New Season Wizard — Step 2 of 6: Activity Rates
+
+ + +
+
+
+
+
Season Info
+
+
+
+
2
+
Activity Rates
+
+
+
+
3
+
Objectives
+
+
+
+
4
+
Tiers
+
+
+
+
5
+
Leaderboard
+
+
+
+
6
+
Review
+
+
+
+ + +
+ + +
+
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 TypePts / UnitScaleEffective 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/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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +