Tech Teardown · Reverse-Engineering Dossier

Ore Factory Squad — Anatomi Teknis

Pembongkaran stack teknologi sebuah game mining/factory co-op skala besar, dan cara game ini menyelesaikan tiga hal yang jadi titik lemah tim: multiplayer, terraforming, dan optimasi + save/load ukuran besar.

engine Unity 6000.3.13f1
scripting IL2CPP (meta v39)
render URP · GPU-driven
net Mirror + Steam
publisher threeW
codename DiggingProject
assets ~13 GB

Kenapa dotPeek tidak jalan di sini — dan apa yang dipakai sebagai gantinya

Game ini di-build dengan IL2CPP, bukan Mono. Tidak ada Assembly-CSharp.dll managed; kode C# sudah diterjemahkan ke C++ lalu native machine code di dalam GameAssembly.dll (95 MB). dotPeek/ILSpy/dnSpy tidak punya IL untuk dibaca. Analisis ini memakai metadata IL2CPP (global-metadata.dat, 24 MB, tidak dienkripsi — magic AF1BB1FA) yang menyimpan seluruh nama namespace, class, method, dan field sebagai teks. Dari situ + manifest assembly (ScriptingAssemblies.json) arsitektur bisa direkonstruksi. Untuk stub C# lengkap, metadata v39 ini hanya didukung Cpp2IL — Il2CppDumper berhenti di v31 (lihat Bagian C); untuk aset, AssetRipper / AssetStudio.

Bagian A — Inventori Teknologi & Sumber

Apa yang berjalan di dalamnya

Tiap library/aset pihak ketiga yang terdeteksi di build, perannya, dan sumber resminya. Semua link sudah diverifikasi.

Engine & Rendering
TeknologiPembuatPeranSumber
Unity 6.3 (6000.3.13f1)UnityEngine, backend IL2CPP, render URPDocs
URP + GPU Resident DrawerUnityRender pipeline, batching GPU-drivenDocs
FSR 3 / FSR 4 / DLSS UpscalingThe Naked DevTiga upscaler resolusiSitus · FSR3 · FSR4 · DLSS
Horizon Based Ambient OcclusionMichaël JimenezAO screen-space teroptimasiAsset Store
LineworkAlexander AmeyeOutline & edge detection (URP)Situs · Asset Store
Translucent ImageLe Loc TaiBlur latar UI berperformaSitus · Asset Store
URP Volumetric FogCristianQiu (open-source)Kabut & pencahayaan volumetrik, mendukung Render Graph Unity 6GitHub · OpenUPM
Multiplayer & Steam
TeknologiPembuatPeranSumber
MirrorMirror NetworkingNetworking HLAPI, host/listen-serverGitHub · Docs
kcp2k · Telepathy · SimpleWebMirror NetworkingTransport UDP / TCP / WebSocketkcp2k · Telepathy · SimpleWeb
FizzySteamworksChykaryTransport Mirror via Steam P2P/relayGitHub · Docs
Steamworks.NETRiley LabrecqueWrapper C# untuk Steam APIGitHub
Toolkit for SteamworksHeathen EngineeringLobby, matchmaking, invite, voiceAsset Store · KB
ConcentuslostrombCodec Opus untuk voice chatGitHub
Terraforming, Dunia & AI
TeknologiPembuatPeranSumber
Digger / Digger PROofuxTerraform voxel (marching cubes) di atas Unity TerrainAsset Store · Docs
Unity Terrain + Splines + ProBuilderUnityTerrain dasar, spline, CSG meshDocs
BurstTriangulatorandywieckoTriangulasi Delaunay ber-BurstGitHub
Clipper2Angus JohnsonClipping/offset poligon 2DGitHub
A* Pathfinding Project ProAron GranbergPathfinding pekerja & kendaraanSitus · Asset Store
Urban Traffic & Pedestrian SystemGleyAI lalu lintas & pejalan kakiSitus · Asset Store
Simple Car Controller (SCC)BoneCracker GamesKontroler kendaraan berbasis fisikaSitus · Asset Store
Optimasi & Lingkungan
TeknologiPembuatPeranSumber
Burst · Jobs · Collections · MathematicsUnityKode SIMD native multithread (DOTS core)Docs
OptimizersFImpossible CreationsLOD/culling runtime (renderer, light, fisika, audio)Asset Store
Nature RendererVisual Design CafeGPU instancing vegetasiSitus · GitHub
UnityMeshSimplifierWhinarnDecimasi mesh / LOD otomatisGitHub
UniStormBlack Horizon StudiosCuaca dinamis, awan volumetrikAsset Store · GitHub
Enviro 3 — Sky and WeatherHendrik HauptLangit, awan & cuaca modularAsset Store
The Toby Foliage EngineTobyfredsonShader vegetasi & anginAsset Store
Shatter StoneNV3DAset 3D props tambang (bijih, gemstone, logam)Bundle · Metal Ores
ALINEAron GranbergDebug drawing runtime (Drawing.dll)Situs
Save, Framework, UI & Audio
TeknologiPembuatPeranSumber
NanoSaveFullscreenSave berbasis slot: gzip, backup, metadata + screenshotAsset Store · Docs
Game Creator 2Catsoft WorksFramework gameplay, variabel, save systemAsset Store
Newtonsoft.JsonJames Newton-KingSerialisasi JSON saveGitHub
Settings & Game OptionsKAMGAMGenerator menu opsi terpaduAsset Store · Manual
I2 LocalizationInter IllusionSistem lokalisasiAsset Store · Manual
Tasty Pie MenuXAMIN SoftwareRadial / pie menu UIAsset Store
NAudioMark HeathPemrosesan audio (.NET)GitHub
Cinemachine · Visual Scripting · Input System · Timeline · RecorderUnityKamera, scripting visual, input, dsb.Daftar paket Unity

Seluruh library pihak ketiga yang terdeteksi sudah teridentifikasi dan link sumbernya diverifikasi. Mayoritas aset berbayar dari Asset Store, sisanya open-source (GitHub/OpenUPM).

Bagian B — Tiga Titik Lemah Tim

Bagaimana game ini menanganinya

01

Multiplayer

Co-op host/listen-server lewat Steam — tanpa server dedicated, tanpa port-forwarding. Satu pemain jadi host, sisanya join via lobby Steam.

Stack

Mirror sebagai HLAPI (abstraksi [Command] [ClientRpc] SyncVar di atas transport), FizzySteamworks sebagai transport di atas Steam P2P/relay, dan Heathen Steamworks untuk lobby + matchmaking. Ada NetworkRoomManager/NetworkLobbyManager (room dulu, baru masuk game), plus NetworkDiscovery untuk LAN.

Cara kerja
  • Join lewat Steam lobby + lobby code (SetSteamLobbyID, currentSteamLobbyID, invite). Identitas pemain dari Steam: NetworkplayerSteamId.
  • Dunia besar di-load additive & disinkron progresnya antar client: AdditiveNetworkManager, NetworkLoadingSync — semua nunggu sampai scene siap.
  • Aksi yang mengubah dunia bersifat server-authoritative: client minta, server yang eksekusi lalu broadcast. Contoh menggali: Cmd_RequestDigSingle → server apply → sync ke semua.
  • State frekuensi tinggi di-bit-pack manual untuk hemat bandwidth: TrafficStatePacker (FloatIntUnion), CompressQuaternion, CompressVarInt.
  • Voice chat dua jalur: Steam voice (GetVoice/DecompressVoice) dan Opus via CmdSendVoiceData/RpcReceiveVoiceData.
Ambil ini
  • Pakai HLAPI matang (Mirror/FishNet) — jangan tulis socket sendiri. Command/Rpc/SyncVar menghemat berbulan-bulan.
  • Transport Steam relay = zero port-forward, zero biaya server. Ini alasan co-op indie bisa "tinggal main".
  • Buat semua mutasi dunia server-authoritative dan kirim sebagai intent (Command), bukan hasilnya.
  • Bit-pack hanya data yang sering berubah; sisanya biarkan SyncVar.
02

Terraforming

Bukan voxel dari nol. Unity Terrain sebagai dasar, Digger PRO menempelkan lapisan voxel untuk lubang, gua, dan overhang yang bisa digali real-time.

Stack

Digger PRO — voxel + MarchingCubesPolygonizer di atas Unity Terrain, dengan generasi mesh di Burst Jobs (AdvancedVoxelGenerationJob) dan native collections. Ditambah BurstTriangulator dan UnityMeshSimplifier untuk LOD hasil galian.

Cara kerja
  • Dunia dibagi chunk voxel: ChunkVoxelPosition, CalculateTerrainChunkLayout — hanya chunk terdampak yang di-regenerate.
  • Setelah menggali, voxel mengambang dideteksi & dibuang lewat connected-component labeling (VoxelPhysics.ConnectedComponentLabeling, HandleFloatingVoxels) — jadi tanah tidak menggantung tak wajar.
  • Gameplay dibungkus konsep "Digsite" (zona tambang): digBrush, digDepthThreshold, DigableAreaCheck, gua tersimpan sebagai CaveSystemSaveData.
  • Menggali diintegrasikan ke netcode: server yang apply (Cmd_RequestDigSingle), bukan tiap client bikin mesh sendiri.
Ambil ini
  • Jangan bikin engine voxel sendiri. Digger di atas Unity Terrain memberi 80% hasil: gua, overhang, galian real-time.
  • Jalankan generasi/triangulasi mesh di Burst Jobs off-main-thread, per-chunk, hanya yang berubah.
  • Simpan delta galian saja (edit voxel), bukan seluruh field — lihat bagian save di bawah.
  • Kirim galian sebagai perintah ke server; hasil mesh direkonstruksi tiap client dari delta yang sama (deterministik).
03

Optimasi & Save/Load Besar

Dua sisi: menjaga frame-rate di dunia besar, dan menulis/membaca save yang besar tanpa nge-freeze — dengan trik "simpan delta, bukan dunia".

Optimasi — Stack
  • Burst + Jobs: kode data-parallel jadi SIMD native (voxel, pathfinding).
  • GPU Resident Drawer (URP GPU-driven) + Nature Renderer untuk instancing vegetasi.
  • FImpossible Optimizers: LOD/culling runtime otomatis untuk renderer, light, partikel, rigidbody, audio.
  • Tiga upscaler (DLSS4/FSR3/FSR4) + HBAO + gfx-jobs multithread (boot.config) + incremental GC (gc-max-time-slice=3).
  • A* Pathfinding dengan graph di-cache/serialize, bukan NavMesh murni.
Save/Load — Stack
  • NanoSave (cepat, async) + Game Creator 2 SaveSystem (slot, metadata, screenshot, playtime, versi).
  • Format JSON + gzip (save.json.gz) + CRC32 + backupSaveFile.
  • Deteksi korup: CorruptedSaveSlot. Save SP & MP terpisah: ShowSaveSlotMultiplayer.
Cara kerja save besar (inti untuk tim)
  • Per-sistem, bukan per-GameObject. Tiap manager serialisasi struct ringkasnya sendiri: ConveyorBeltSaveData, BankSaveData, ContractManagerSaveData, CaveSystemSaveData, dst. Dunia direkonstruksi saat load, bukan menyimpan scene mentah.
  • Segmented + coroutine. Save dipecah jadi segmen yang ditulis lintas frame: _numberOfSegmentsForMostRecentSave, NanoSaveCoroutineHelper — tidak ada freeze walau save besar.
  • Simpan delta, bukan dunia. Terrain dibangkitkan prosedural (GenerateTerrainChunks); yang disimpan hanya edit galian + objek yang ditaruh pemain. Ini yang bikin save tetap kecil di dunia raksasa.
  • Satu serializer, dua fungsi. Save yang sama dikirim host ke pemain yang baru join sebagai file snapshot: Co_SendFileSnapshotTo, Co_ApplyFileSnapshot. Persistence dan late-join pakai jalur serialisasi yang sama.
Ambil ini
  • Save berbasis komponen: tiap sistem punya struct SaveData-nya sendiri + versi. Skalabel, mudah migrasi.
  • Tulis async/segmented lintas frame → save besar tanpa hitch.
  • Simpan seed + delta, regenerate sisanya deterministik. Ini kunci save kecil di world besar.
  • gzip + CRC + file backup itu wajib, bukan opsional. Dan pakai ulang serializer save sebagai snapshot late-join multiplayer.
  • Menang cepat untuk perf: pasang upscaler (DLSS/FSR) dan LOD manager asset sebelum optimasi manual.
Bagian C — Verifikasi dari Source Ter-decompile

Bukti di level kode

Metadata Unity 6.3 memakai format baru (v39) yang belum didukung Il2CppDumper. Dump berhasil dengan Cpp2IL → 162 stub DLL (bisa dibuka di dotPeek) + Assembly-CSharp.dll di-decompile jadi ~57.500 baris C#. Dua pola ini terkonfirmasi dari kode asli, bukan tebakan.

Pola save: interface IGameSave

Setiap sistem gameplay yang perlu disimpan meng-implement satu kontrak — bukan sekadar "serialize", tapi lengkap dengan identitas & mode load:

interface IGameSave {
    string   SaveID   { get; }   // kunci unik per sistem
    bool     IsShared { get; }   // state dunia (shared) vs per-pemain
    Type     SaveType { get; }   // tipe objek *SaveData
    LoadMode LoadMode { get; }   // enum: kapan/bagaimana di-load
    object   GetSaveData(bool includeNonSavable);  // serialize
    Task     OnLoad(object value);                 // deserialize + apply (async)
}

Yang meng-implement (semua sekaligus NetworkBehaviour): FactoryManager T_ConveyorBelt CaveSystemManager T_MinerVehicle DigSiteHazardManager FactoryCustomizationManagerSaveLoadGameManager enumerasi semua, panggil GetSaveData(), lalu saat load panggil OnLoad(value) async (Task) supaya sistem berat tidak nge-block. Flag includeNonSavable membuat satu method melayani dua mode (save penuh vs capture ringan).

Sync terraform: DiggerReplayMessenger

Menggali disinkron ke client dengan dua strategi lewat Mirror NetworkMessage:

1 — Replay operasi (ringan, deterministik)

struct ReplayOp { Vector3 pos; byte brush; byte action; float size, opacity; sbyte textureIndex; } — tiap galian jadi op mungil (perhatikan tipe hemat: byte/sbyte). Dikirim batch via ReplayChunkMsg(List<ReplayOp>) dengan ReplayBeginMsg{startIndex} dan ClientReadyForReplayMsg{lastKnownIndex} → client hanya menerima op setelah yang sudah dimilikinya (catch-up inkremental), lalu memutar ulang untuk merekonstruksi terrain.

2 — File snapshot (state penuh, terkompresi)

FileSnapshotBeginMsg{totalFiles}FileDataMsg{fileName, byte[] compressedData, uncompressedSize} per file → FileSnapshotEndMsgFileBatchAckMsg. File voxel Digger dikirim gzip, per-batch, dengan ack, diterapkan lewat coroutine Co_ApplyFileSnapshot.

Pelajaran: kirim intent kecil saat bisa (op-list deterministik), fallback ke transfer file terkompresi berbatch untuk state besar. Persistence & late-join memakai serializer yang sama.

Orkestrasi jaringan & save

Dari NewNetworkManager : NetworkManager dan SaveLoadGameManager — lima pola yang bikin dunia besar + multiplayer tetap stabil.

  • Late-join = load scene → stream terrain. OnServerAddPlayerHandlePlayerLoadingCompletion(conn), dan di dalamnya server menyerahkan ke DiggerReplayMessenger untuk men-stream state galian ke pemain baru. Traffic juga: SendTrafficInitialStateNextFrame.
  • Load bergerbang reference-count. RegisterPendingLoadOperation(reasonKey) / CompletePendingLoadOperation + _activeReasons — tiap subsistem mendaftar "masih loading"; UI hilang saat semua selesai (HasPendingOperations). Cara bersih menangani load multi-sistem tanpa menebak "kapan siap".
  • Freeze fisika saat load. RegisterKinematicForLoad(Rigidbody)ReleaseAllKinematics() — rigidbody dibekukan selama world streaming lalu dilepas serentak. Cegah benda nembus lantai sebelum collider siap.
  • Save async + UX multiplayer. Task SaveGame() (non-blocking) dengan ShowSavingPanelOnAllClients() / HideSavingPanelOnAllClients(). Autosave dipicu event OnDayStarted (batas hari), bukan timer sembarang.
  • Wrapper "Safe" + alasan disconnect. StartHostSafe / StartServerOnlySafe / StartClientSafe, dan DisconnectReason + DisconnectReasonMessage → client tahu kenapa terputus (kick, lobby penuh, host stop) dan tampilkan pesan tepat.

Netcode konkret: GamePlayer + tuning stream

GamePlayer — SyncVar + pola Command

State pemain disebar lewat SyncVar (auto server→client, dengan hook): playerName playerSteamId isInDigsite customizationIDs vehicleNetId. Aksi memakai satu pola konsisten — wrapper publik → [Command]Server*[ClientRpc]/[TargetRpc]: mis. NetworkTeleportCmdTeleportRpcTeleportApplyTeleportLocal; damage SendDamage → CmdReceiveDamage → ServerReceiveDamage → TargetReceiveDamage. Semua mutasi lewat server. Voice: field SteamVoiceChat voiceChat per pemain.

DiggerReplayMessenger — knob tuning stream

Nilai yang menentukan mulus-tidaknya transfer terrain besar (semua di Inspector): REPLAY_CHUNK_SIZE (op per pesan), APPLY_PER_FRAME (op diterapkan per frame — anti-hitch), FILES_PER_BATCH, FILE_BATCH_DELAY, COMPRESS_FILE_DATA (gzip), USE_ACK_FLOW_CONTROL + ACK_TIMEOUT (tunggu ack sebelum batch berikut). Per-koneksi dilacak _lastSentIndex/_lastAckReceived untuk catch-up inkremental.

Contoh IGameSave end-to-end: FactoryManager & CaveSystemManager

Dua implementasi nyata — pola save + netcode yang paling layak ditiru tim.

  • Save = struct kecil, dunia direkonstruksi. FactorySaveData hanya { money, currentXP, level, name }; CaveSystemSaveData hanya List<CaveSaveEntry> berisi flag ringkas (aktif/pecah/item-spawned + jumlah node). OnLoad(object) cast balik & terapkan. Tidak ada GameObject/mesh yang di-serialize.
  • Versi per-entry. Tiap CaveSaveEntry.saveVersion + CURRENT_SAVE_VERSION → migrasi save bisa per-item, bukan hanya per-file global.
  • Mutasi 100% server-authoritative. UI panggil AddMoneyCmdAddMoney (client→server) → ServerAddMoney (validasi + tulis SyncVar _money) → auto-propagate → OnMoneyChangedHook update UI di semua client. Client tak pernah ubah uang langsung — anti-cheat + konsisten.
  • Pisahkan angka mentah dari tampilan. SyncVar _money/_currentXP/_level → properti read-only terhitung (XPProgress, RequiredXPForNextLevel) dari LevelConfigSO. UI tidak menyentuh field mentah.
  • State dunia via SyncList + simpan delta. CaveSystemManager menyebar status gua lewat Mirror SyncList (ApplyCaveActive/ApplyDoorBroken), tapi yang disimpan hanya List<CaveSaveEntry> — delta, bukan geometri.
  • Gating demo jadi warga kelas satu. IsDemoLevelLimitReached + CmdSetLevel menolak naik di atas cap demo (Level 3) di sisi server — tak bisa dibypass client.
Bagian D — Pola dari Sistem Lain

Empat pola yang layak dicuri

Dari sistem produksi, pekerja, logistik, dan ekonomi — pola arsitektur yang bisa langsung tim adopsi.

Fondasi: satu model item untuk semua

Item cuma data: ItemStack { string itemId; int count; } — bukan GameObject. Semua yang memegang item mewarisi satu base:

abstract class ItemContainerBase : NetworkBehaviour, IItemContainer {
    protected SyncList<ItemStack> storedItemStacks;   // sumber kebenaran (auto-sync)
    protected Dictionary<string,int> itemCounts;       // cache turunan (perf)
    public virtual void ServerSetItems(...); ServerRemoveItems(...); ServerClear();
}

Storage, mesin, kendaraan, pallet — semua turunan ItemContainerBase, jadi dapat inventory yang networked + savable "gratis". Peran ditambah lewat interface kecil: IItemContainer (punya item), ICargoSource (bisa diambil cargo-nya + ServerTakeLastSackData), IEntityInfo (nama/status untuk UI). Contoh komposisi: T_MinerVehicle : NetworkBehaviour, IGameSave, ICargoSource, IItemContainer, IEntityInfo.

  • Model item sebagai id + count di SyncList, bukan objek. Pindah item = matematika dictionary; save = dump list.
  • Satu base container + interface kecil yang dikomposisi mengalahkan hierarki warisan yang dalam.

Pekerja / "squad": AI state-machine 100% server

T_Employee — SyncVar-nya hanya identitas + state (employeeId type _workState _isCarrying _isWorking), bukan posisi. AI berjalan sebagai state machine server: EnterState + ServerUpdateIdle / GoingToTruck / PickingUp / GoingToDropOff / Dropping / ReturningHome / Error. Gerak pakai A* FollowerEntity lokal di tiap client + recovery macet (HasPathFailed, RetryReachDestination, RequestUnstuck). Cargo diambil via ICargoSource dengan sistem reservasi (GetCargoReservations/ReleaseCargoReservation) supaya dua pekerja tak rebutan. Save = EmployeeSaveData{ employeeId, position, rotation, isWorking }.

  • AI agen jalan hanya di server; sync cuma enum state kecil. Posisi direkonstruksi client-side dari pathfinding, tidak di-sync — hemat bandwidth besar untuk banyak agen.
  • Reservasi sumber mencegah konflik saat banyak agen memperebutkan target yang sama.

Conveyor: spline + item diposisikan (lane, t)

Belt = SplineContainer + speed. Item tidak punya transform sendiri di belt — posisinya parameter (laneIndex, t) sepanjang spline (EvaluateOnSpline(lane, t, out pos, out tan), multi-lane via GetLaneCount). Server yang mendaftar item: Server_RegisterItem(item, startT, laneIndex). Belt nyambung otomatis by jarak (FindNextBelt + connectionRadius + RefreshAllConnections) — taruh belt, langsung tersambung. Splitter pakai round-robin (GetNextSplitterExitIndex). Save = ConveyorBeltSaveData{ bool[] savedExitStates } + UniqueConveyorBeltId — cuma state gerbang + id stabil.

  • Transport item data-oriented: posisi = parameter kurva, bukan ribuan Transform/Rigidbody — jauh lebih ringan.
  • Bangunan auto-connect by proximity + id stabil untuk save → tak perlu simpan graf koneksi.

Ekonomi dinamis: feedback loop server

MarketPriceManager menyimpan multiplier harga per item. Jual → OnItemSoldServerApplyPriceChange menurunkan harga (decreasePerSale), dibatasi [minPriceMultiplier, maxPriceMultiplier]. Tiap hari OnDayStartedServerApplyDailyDecay menarik harga kembali ke baseline. Save = List<ItemPriceModifierSaveEntry{ itemId, priceMultiplier }>.

  • Ekonomi sebagai loop umpan-balik server (jual → harga turun → decay harian pulih), bounded agar tak lari.
  • Simpan hanya delta (multiplier per item), bukan seluruh tabel harga.