← Back to Blog

Smart Assemblies Expansion: Tracking Portable Structures, Totems, and Tribal Markers

EVE Frontier's recent patches added 19 new deployable structure types—portable printers, decorative totems, tribal walls, and more. For EF-Map, this meant a significant expansion of our Smart Assemblies tracking system, requiring careful database analysis, backend schema updates, and UI enhancements while maintaining zero breaking changes for existing users.

This post details our phased approach to expanding coverage from 6 assembly types to 25+, improving tribe-based filtering, and fixing edge cases in star coloring—all delivered through preview deployments and validated with real player data.

The Discovery: 9,736 Unmapped Assemblies

Identifying the Gap

Our Smart Assemblies snapshot initially tracked six categories:

But our indexer also reported 9,736 assemblies classified as generic "smart_assembly"—structures we were ingesting but not categorizing.

Players started asking:

Time to dig into the database.

Phase 1: Postgres Schema Discovery

We queried our PostgreSQL indexer (fed by blockchain events) to enumerate all deployable types in the game data:

SELECT type_id, type_name, db_category, db_group, COUNT(*) as instances
FROM world_api_dlt.types t
JOIN chain_schemas.evefrontier__smart_assembly a ON a.type_id = t.type_id
WHERE t.db_category = 'Deployable'
GROUP BY type_id, type_name, db_category, db_group
ORDER BY instances DESC;

Results: 33 deployable types in the database, of which only 6-10 were properly classified in our snapshot.

The missing types fell into clear categories:

Portable Structures (high player demand):

Cosmetics / Decorative (visual markers):

Additional Manufacturing:

These 19 new types accounted for most of the "unmapped" assemblies and represented features players actively used in-game.

Architecture Challenge: Aggregated Counts Model

Understanding the Snapshot Structure

Our Smart Assemblies snapshot doesn't store individual assembly records. Instead, it uses an aggregated counts model:

{
  "meta": {
    "totalAssemblies": 52008,
    "types": { "manufacturer": 32341, "NWN": 4061, ... },
    "statuses": { "2": 43067, "3": 7990, "4": 951 }
  },
  "systems": {
    "30000004": {
      "counts": { "manufacturer": { "2": 2, "3": 1 } },
      "tribes": { "1000167": { "manufacturer": { "2": 2 } } }
    }
  }
}

Structure: systems[solarSystemId].counts[assemblyType][status] = count

This compression keeps the snapshot compact (~500 KB vs potential 5+ MB for individual records), but means we cannot filter by specific type IDs client-side. All classification must happen in the backend exporter.

The Implication

To support new assembly types, we needed to:

  1. Update the snapshot exporter (Node.js script querying Postgres)
  2. Add type classification logic mapping type IDs → category labels
  3. Extend the frontend to display new filter toggles

No client-side type ID matching possible—this was a backend-first change.

Phased Implementation Strategy

Phase 1: Schema Discovery & Categorization (Complete)

We documented all 33 deployable types with proposed groupings:

Category Types Default Visibility Rationale
Portable Structures 4 types Enabled High player demand; tactical relevance
Cosmetics 10 types Disabled Visual clutter; lower priority for intel
Manufacturing (expanded) Existing + 7 new Enabled Industry tracking
Infrastructure (existing) NWN, Gates, Beacons Enabled Critical for navigation
Storage (existing) SSU, Hangars Enabled Resource control points
Defense (existing) Smart Turrets Enabled Threat awareness

This taxonomy balanced gameplay utility (portable structures for logistics) with visual clarity (cosmetics off by default to reduce map noise).

Phase 2: Backend Exporter Update

We extended tools/snapshot-exporter/exporter.js with type classification:

const TYPE_CLASSIFICATION = {
  // Portable Structures
  87160: 'portable', 87161: 'portable', 87162: 'portable', 87566: 'portable',
  
  // Cosmetics
  88098: 'cosmetic', 88099: 'cosmetic', 88100: 'cosmetic', 88101: 'cosmetic',
  89775: 'cosmetic', 89776: 'cosmetic', 89777: 'cosmetic', 89778: 'cosmetic',
  89779: 'cosmetic', 89780: 'cosmetic',
  
  // Manufacturing (new additions)
  87119: 'manufacturer', 87120: 'manufacturer', 88067: 'manufacturer',
  88063: 'manufacturer', 88064: 'manufacturer', 88068: 'manufacturer',
  88069: 'manufacturer', 88070: 'manufacturer', 88071: 'manufacturer',
  
  // Existing types remain unchanged
};

function classifyAssembly(typeId) {
  return TYPE_CLASSIFICATION[typeId] || 'smart_assembly';
}

This explicit mapping ensured we could extend gracefully—new types added to the game require only appending to this object.

Validation: Ran exporter with DRY_RUN=1, confirmed:

Phase 3: Frontend Type Support

Extended React component constants:

// Before (6 types)
const SMART_ASSEMBLY_TYPES = [
  'manufacturer', 'smart_hangar', 'NWN', 'SG', 'SSU', 'ST'
] as const;

// After (8 types)
const SMART_ASSEMBLY_TYPES = [
  'manufacturer', 'smart_hangar', 'NWN', 'SG', 'SSU', 'ST',
  'portable', 'cosmetic'  // NEW
] as const;

const SMART_ASSEMBLY_TYPE_LABELS: Record = {
  manufacturer: 'Manufacturers',
  smart_hangar: 'Smart Hangars',
  NWN: 'Network Nodes',
  SSU: 'Smart Storage Units',
  ST: 'Smart Turrets',
  SG: 'Smart Gates',
  portable: 'Portable Structures',    // NEW
  cosmetic: 'Decorative Structures'   // NEW
};

UI now renders two additional filter chips in the Smart Assemblies panel. Clicking "Portable Structures" colors stars containing portable printers, storage, etc.

Tribe Coloring Fixes

Issue 1: "Other" Category Didn't Color Stars

When tribe color mode was active, the top-10 tribes got individual colors from a palette:

Clicking individual tribe chips worked perfectly. But clicking "Other" resulted in zero stars colored.

Root Cause: Halo generation code tried to look up tribeTotals.get('other'), but non-top-10 tribes were stored under their actual tribe IDs, not a synthetic "other" key. The count always returned 0.

Fix: Added aggregation logic when tribeId === 'other':

if (tribeId === 'other') {
  // Sum counts for all tribes NOT in top-10
  tribeCount = Array.from(tribeTotals.entries())
    .filter(([tid]) => tid === 'other' || !topTribes.has(tid))
    .reduce((sum, [, count]) => sum + count, 0);
} else {
  tribeCount = tribeTotals.get(tribeId) || 0;
}

Now clicking "Other" correctly highlights 460 systems with 4,381 assemblies from non-top-10 tribes.

Issue 2: Destroyed Status Shows Only "Other" (Data Limitation)

When filtering to Destroyed status (state=4), the tribe legend collapsed to just "Other" with all destroyed assemblies aggregated.

Investigation via custom diagnostic script:

Total destroyed assemblies: 951
With owner data: 0 (0.0%)
With tribe data: 0 (0.0%)
Sample: All records show account=null, tribe_id=null

Conclusion: When an assembly transitions to destroyed, ownership data is removed from the blockchain state (or our indexer doesn't preserve historical ownership for state=4 entries). External tools showing destroyed item owners likely use different data sources or historical snapshots.

This is a data limitation, not a code bug. Tribe-based coloring for destroyed structures is impossible with the current indexing model.

Decision: Document as known limitation; no exporter changes required. Future enhancement could involve archiving ownership snapshots before state transitions.

Performance Validation

Zero Impact from New Features

After tribe coloring fixes and type expansion, we tested performance extensively:

Baseline (before changes):

Post-fix (with "Other" aggregation + new types):

Snapshot size:

Conclusion: Aggregation logic and new type categories add no measurable overhead. The extra dependencies in React's useLayoutEffect (recommended best practice) trigger re-renders only when relevant state changes.

Testing & Validation Matrix

We validated the expansion through systematic testing:

Scenario Expected Behavior Status
Toggle "Portable Structures" Stars with portable printers/storage color correctly ✅ Pass
Toggle "Cosmetics" Totem/wall deployments appear; counts accurate ✅ Pass
Combine new + existing types No duplicate counts; filters independent ✅ Pass
"Other" tribe click (Online status) 460 systems colored with yellow/orange halos ✅ Pass
"Other" + top tribe multi-select Ctrl+click combines correctly; best-tribe logic works ✅ Pass
Destroyed status filtering Tribe legend shows only "Other" (known limitation) ✅ Expected
Snapshot refresh New types persist; old types unchanged ✅ Pass
localStorage persistence Type selections saved/restored across sessions ✅ Pass

Deployment & Rollout

Preview-First Strategy

Following our standard workflow:

  1. Backend exporter test: Ran with DRY_RUN=1, validated counts
  2. KV snapshot publish: Uploaded new snapshot to Cloudflare KV (non-production namespace)
  3. Frontend preview deploy: Cloudflare Pages preview branch with updated UI
  4. Validation: Tested filtering, tribe coloring, performance on preview URL
  5. Production promotion: Merged to main after all gates passed

No production downtime. Users experienced seamless upgrade—new filter chips simply appeared in the panel.

Lessons for Phased Expansion

What Worked

1. Database-first discovery

Querying the blockchain indexer revealed the full scope before writing code. We didn't guess which types to add—we enumerated everything and categorized deliberately.

2. Explicit type mapping over heuristics

Using a simple TYPE_CLASSIFICATION object (type ID → category label) made the exporter logic transparent and easy to extend. Future types require one-line additions.

3. Aggregated data model = graceful schema evolution

Because the snapshot uses flexible JSON keys (counts[anyType][anyStatus]), adding new types didn't break existing consumers. Frontend components adapted automatically.

4. Default visibility choices respect UX

Enabling "Portable Structures" by default (high tactical value) while disabling "Cosmetics" (visual clutter) prevented overwhelming users. Players can opt-in to decorative markers.

5. Performance testing before rollout

Measuring FPS and render time ensured new features didn't degrade experience. The tribe aggregation fix passed performance gates despite adding iteration logic.

What We'd Improve

Type ID documentation: We should maintain a living reference document mapping type IDs to human-readable names and categories. Currently, this knowledge lives in the expansion plan markdown—should be extracted to a shared CSV or JSON for reuse.

Automated type discovery: The exporter could query the database directly for type metadata instead of hardcoding IDs. This would make new game patch types auto-discoverable (though classification logic would still need manual curation).

Ownership archival for destroyed assemblies: To support tribe coloring on destroyed structures, we'd need historical ownership snapshots. This requires upstream indexer changes to preserve account/tribe before state transitions.

Conclusion: Extensible by Design

Expanding Smart Assemblies tracking from 6 to 25+ types demonstrated the value of flexible data architecture:

When EVE Frontier's next patch adds more deployable types, we'll simply:

  1. Query Postgres for new type IDs
  2. Append to TYPE_CLASSIFICATION
  3. Add labels to frontend (if new category)
  4. Deploy via preview → validate → promote

No architectural changes. No breaking migrations. Just extend and ship.

That's the payoff of designing for expansion from the start.

Related Posts

smart assemblies deployable structures blockchain indexing data expansion tribe filtering phased rollout schema evolution performance optimization