- C# 42.6%
- TypeScript 13.9%
- Rust 13.1%
- HTML 10.7%
- Python 10.6%
- Other 9.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Introduced Newtonsoft.Json-backed persistence. Improved backing-store reliability and serialization across the caching stack. Improved networking security posture. Key changes: - Add new GameCult.Caching.NewtonsoftJson project with SingleFile/MultiFile backing stores and polymorphic known-type envelopes. - Add MessagePack helper serialization (DatabaseEntrySerialization) and update MessagePack backing stores to use the shared options/resolver; add DatabaseLink formatter and generator/analyzer docs. - Update CultLib.sln to include the new projects and new caching tests project. - Improve CultCache persistence behavior: track pending store operations, wait for operations on PullAll, and centralize async push handling with error logging. - Harden file I/O in multi-file/single-file stores: atomic temp-file writes, safe Replace/Move semantics, rename-safe deletes, and improved file-watcher subscription and debounce disposal. - Add README files and documentation for several subprojects and packaging projects. These changes add JSON persistence support, improve serialization consistency, and make on-disk operations more robust and observable. |
||
| .vscode | ||
| src | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| CultLib.sln | ||
| CultLib.txt | ||
| Directory.Build.props | ||
| LICENSE | ||
| README.md | ||
CultLib
CultLib is a set of reusable C# libraries for game backends, game-adjacent services, and Unity-integrated tooling.
The libraries cover three main areas:
- logging primitives and implementations
- a typed in-memory cache with pluggable persistence
- LiteNetLib-based networking with encrypted credential exchange and signed session tokens
Repository Scope
The solution includes:
GameCult.Logging: common logging abstraction plus console and file implementationsGameCult.Caching:DatabaseEntry-based cache, indexes, global entries, and backing-store abstractionsGameCult.Caching.MessagePack: MessagePack-backed persistence for the cacheGameCult.Caching.NewtonsoftJson: Newtonsoft.Json-backed persistence for the cacheGameCult.Caching.MessagePack.Generator: source generator for MessagePack formatters for cache modelsGameCult.Caching.MessagePack.Analyzers: packaging project that delivers the generator to consuming projectsGameCult.Networking: encrypted login/register/verify flows and message dispatch over LiteNetLibGameCult.Caching.Tests: NUnit tests for cache and backing-store behaviorGameCult.Networking.Tests: NUnit tests for networking behaviorGameCult.Unity: Unity-side project and integrations used with the runtime libraries
Repository Layout
src/
GameCult.Logging/
GameCult.Caching/
GameCult.Caching.MessagePack/
GameCult.Caching.NewtonsoftJson/
GameCult.Caching.MessagePack.Generator/
GameCult.Caching.MessagePack.Analyzers/
GameCult.Networking/
GameCult.Unity/
tests/
GameCult.Caching.Tests/
GameCult.Networking.Tests/
Build
dotnet build CultLib.sln
Test
dotnet test CultLib.sln
Common Concepts
DatabaseEntry
The cache-centric libraries revolve around DatabaseEntry. Every entry has a stable Guid identifier and can optionally:
- expose a human-readable name through
INamedEntry - participate in generic indexes registered at runtime
- be treated as a global singleton entry through
GlobalSettingsAttribute
Typical entry shape:
using GameCult.Caching;
public class ItemData : DatabaseEntry, INamedEntry
{
public string Name = string.Empty;
public int Value;
public string EntryName
{
get => Name;
set => Name = value;
}
}
CultCache and Backing Stores
CultCache is an in-memory index over DatabaseEntry objects. It can operate entirely in memory, or it can be attached to one or more backing stores for persistence and synchronization.
- the cache is the query surface
- backing stores are persistence adapters
- indexes and name maps are maintained inside the cache, not inside the store
Important: Multiple Backing Stores
When multiple backing stores are added, behavior depends on how they are registered.
If a store is added with domain types:
cache.AddBackingStore(playerStore, typeof(PlayerData));
cache.AddBackingStore(settingsStore, typeof(AppSettings));
then that store becomes the direct persistence target for those types.
If a store is added without domain types:
cache.AddBackingStore(primaryStore);
cache.AddBackingStore(mirrorStore);
then the first generic store acts as the primary writable store for non-domain-specific entries. Additional generic stores subscribe to the existing stores and mirror their change events.
Implications:
- order matters for generic stores
AddAsyncwrites to the type-specific store when one exists- otherwise
AddAsyncwrites to the first generic store - later generic stores do not become co-primaries; they mirror earlier stores
PullAllBackingStoresAsyncpulls from every registered store
Recommended patterns:
- use one generic primary store if you want simple persistence
- use domain-specific stores when different entry types belong in different persistence layers
- treat additional generic stores as mirrors or downstream replicas, not independent write targets
Example: Cache + Networking
using GameCult.Caching;
using GameCult.Logging;
using GameCult.Networking;
var cache = new CultCache
{
Logger = new ConsoleLogger()
};
var server = new Server(cache)
{
Logger = new ConsoleLogger()
};
server.Start();
Secrets and Runtime Configuration
GameCult.Networking uses two pieces of deployment configuration:
GAMECULT_CONNECTION_KEYGAMECULT_SESSION_SIGNING_SECRET
Production guidance:
- set both values before starting the server
- treat
GAMECULT_CONNECTION_KEYas shared client/server protocol configuration - treat
GAMECULT_SESSION_SIGNING_SECRETas a server-only secret - generate high-entropy random values and store the server-side value in your platform's secret store
- do not rely on local-development defaults in production
- partial configuration is rejected at startup
Recommended value format:
- at least 32 random bytes per value
- encode as Base64 or Base64Url if you want a portable text representation
Example secret generation in PowerShell:
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
Typical production setup patterns:
- inject
GAMECULT_SESSION_SIGNING_SECRETand the server copy ofGAMECULT_CONNECTION_KEYas environment variables from Docker secrets, Kubernetes secrets, systemd environment files, or your cloud secret manager - ship the matching
GAMECULT_CONNECTION_KEYto the client through a build-generated config asset, code-generated constants, or another explicit client configuration mechanism - scope them per environment so development, staging, and production do not share values
- rotate
GAMECULT_SESSION_SIGNING_SECREToperationally - treat
GAMECULT_CONNECTION_KEYas versioned protocol config; changing it requires a coordinated client/server rollout
Example server startup with strict environment validation:
using GameCult.Caching;
using GameCult.Networking;
var cache = new CultCache();
var security = ServerSecurityOptions.FromEnvironment();
var server = new Server(cache, security);
Example local-development server startup:
using GameCult.Caching;
using GameCult.Networking;
var cache = new CultCache();
var security = ServerSecurityOptions.Development();
var server = new Server(cache, security);
Example shipped-client startup with explicit client configuration:
using GameCult.Networking;
var security = new ClientSecurityOptions("<matching-connection-key>");
var client = new Client(security);
Project Docs
Each subproject has a local README with package-specific detail: