π€ AI Assistance Disclosure:
This project has been actively developed and maintained by human authors for over a decade. Recent versions have utilized AI assistance for documentation, test coverage, and performance optimizations.
DxMessaging is a type-safe messaging system that replaces sprawling C# events, brittle UnityEvents, and global static event buses with an observable and lifecycle-managed communication pattern.
Think of it as: A messaging system designed for decoupled game systems.
Need install instructions? Try OpenUPM (recommended) or see the Install Guide for Git URLs, scoped registries, and more.
- 30-Second Elevator Pitch
- Mental Model: How to Think About DxMessaging
- Quick Start (5 Minutes)
- Dependency Injection (DI) Compatible
- Is DxMessaging Right for You?
- Why DxMessaging
- Key Features
- The DxMessaging Solution
- Real-World Examples
- Performance
- Comparison Table
- Samples
- Documentation
- Requirements
- Contributing
- Links
- Forgotten to unsubscribe from an event and spent hours debugging memory leaks
- Had UI code tangled with 15 different game systems
- Wondered "which event fired when?" with no way to see message flow
- Copy-pasted event boilerplate dozens of times
- Automatic lifecycle management - manages subscription lifecycle automatically, no manual unsubscribe
- Zero coupling - systems communicate without knowing each other exist
- Full visibility - see every message in the Inspector with timestamps and payloads
- Complete control - priority-based ordering, validation, and interception
- Untargeted - Global announcements
- Targeted - Commands to specific entities
- Broadcast - Observable facts from a source
See Mental Model for how to choose the right type.
One line: It's a type-safe messaging system with automatic lifecycle management and built-in inspection tools.
DxMessaging is built around one principle: it gets out of your way.
You have data. You need to pass it around. That's the problem. DxMessaging provides fast, simple primitives as building blocks. You model changes as message types with optional context, using game primitives (GameObjects, components) as that context.
You don't build your game INTO the messaging system. It's opt-in and optionalβa tool you reach for when it helps.
π‘ Diagrams below require Mermaid support. If they don't render, try viewing this file directly on GitHub.
Each message type maps to a real-world communication pattern:
flowchart LR
S[Someone] -->|announces| PA[π’ PA System]
PA --> L1[Listener A]
PA --> L2[Listener B]
PA --> L3[Listener C]
Announcements with no specific recipient. Everyone who cares can hear it.
Examples: "The game is paused", "Settings changed", "Scene finished loading"
flowchart LR
S[Sender] -->|"To: Player"| Letter[π¬ Message Bus]
Letter --> Player[Player receives]
Other1[Enemy A] -.->|ignores| Letter
Other2[Enemy B] -.->|ignores| Letter
Commands to a specific recipient. Only that entity receives them.
Examples: "Player, heal for 10 HP", "Door #7, open", "This enemy, take 25 damage"
flowchart LR
Source[Enemy] -->|"I took damage!"| Radio[π» Message Bus]
Radio --> L1[Damage Numbers UI]
Radio --> L2[Achievement Tracker]
Radio --> L3[Analytics]
Radio --> L4[Combat Log]
Facts emitted by a specific source. No intended recipientβjust an origin. Anyone can tune in.
Examples: "This enemy took 25 damage", "The player picked up item X", "This chest opened"
flowchart TD
Start([I need to send a message])
Start --> Q1{Does it matter<br/>WHO sent it?}
Q1 -->|No| Q2{Does it matter<br/>WHO receives it?}
Q2 -->|No| Untargeted[Use UNTARGETED<br/>Global announcement]
Q2 -->|Yes| Targeted[Use TARGETED<br/>Directed command]
Q1 -->|Yes| Q3{Am I commanding<br/>someone to act?}
Q3 -->|Yes| Targeted
Q3 -->|No| Broadcast[Use BROADCAST<br/>Observable fact]
| Question | Untargeted | Targeted | Broadcast |
|---|---|---|---|
| Has a specific sender that matters? | β | β | β |
| Has a specific recipient? | β | β | β |
| Is it a command? | β | β | β |
| Is it an observable fact? | Maybe | β | β |
| Is it a global announcement? | β | β | β |
β οΈ Common Mistakes:
- Forgetting to enable the token β Messages won't be received. Use
MessageAwareComponent(auto-enables) or callToken.Enable()manually.- Targeting Component when you meant GameObject β These are distinct registration paths. Component-targeted messages won't reach GameObject-level handlers.
- Using Broadcast when you need Targeted β Broadcasts have no recipient, just an origin. Use Targeted when commanding a specific entity.
- Missing
[Dx*Message]attribute β The source generator won't process the struct without the marker attribute.π See Troubleshooting for solutions to these and other issues.
π Want more depth? See the full Mental Model documentation for detailed examples, lifecycle patterns, and edge cases.
π Ready to code? Jump to Quick Start to send your first message!
New to messaging? Start with the Visual Guide (5 min) for a beginner-friendly introduction!
openupm add com.wallstop-studios.dxmessaging# Unity Package Manager > Add package from git URL...
https://github.com/wallstop/DxMessaging.gitSee the Install Guide for all options including NPM scoped registries and local tarballs.
using DxMessaging.Core.Attributes;
[DxTargetedMessage]
[DxAutoConstructor] // Auto-generates constructor
public readonly partial struct OpenChest {
public readonly int chestId;
[DxOptionalParameter(true)] // Optional with custom default
public readonly bool playSound;
}using DxMessaging.Unity;
public class ChestController : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterComponentTargeted<OpenChest>(this, OnOpen);
}
void OnOpen(ref OpenChest msg) {
Debug.Log($"Opening chest {msg.chestId}");
}
}// From anywhere:
var msg = new OpenChest(chestId: 42);
msg.EmitComponentTargeted(chestComponent);No manual unsubscribe needed. Subscriptions are type-safe and lifecycle-managed.
Stuck? See Troubleshooting or FAQ
Using Zenject, VContainer, or Reflex? DxMessaging is fully DI-compatible out of the box!
// Inject IMessageBus in any class
public class PlayerService : IInitializable, IDisposable
{
private readonly MessageRegistrationLease _lease;
public PlayerService(IMessageRegistrationBuilder builder)
{
// Builder automatically resolves your container-managed bus
_lease = builder.Build(new MessageRegistrationBuildOptions
{
Configure = token => token.RegisterUntargeted<PlayerSpawned>(OnSpawn)
});
}
public void Initialize() => _lease.Activate();
public void Dispose() => _lease.Dispose();
}- DI for construction β Inject services, repositories, managers via constructors
- Messaging for events β Reactive, decoupled communication for gameplay events
- Combined approach β Clean architecture with testable, isolated buses
- β Zenject/Extenject β Full-featured DI with extensive Unity support
- β VContainer β Lightweight, high-performance DI with scoped lifetimes
- β Reflex β Minimal API, high-performance dependency injection
- Zenject Integration Guide β Complete setup with examples
- VContainer Integration Guide β Scoped buses for scene isolation
- Reflex Integration Guide β Minimal, lightweight patterns
- Runtime Configuration β Setting message buses at runtime, re-binding registrations
- Message Bus Providers β Provider system for design-time and runtime bus configuration
Not using DI? No problem. DxMessaging works standalone with zero dependencies.
- You have cross-system communication - UI needs to react to gameplay, achievements track events, analytics observe everything
- You're building for scale - 10+ systems that need to communicate, or growing from prototype to production
- Memory leaks are a concern - You've been bitten by forgotten event unsubscribes before
- You value observability - Need to debug "what fired when?" or track message flow
- Teams/long-term maintenance - Multiple developers, or you'll maintain this code for years
- You want decoupling - When UI classes need references to many game systems
- You're using DI frameworks - Compatible with Zenject/VContainer/Reflex (see DI Compatible)
- Tiny prototypes/game jams - If your game is <1000 lines and will be done in a week, C# events are fine
- Simple, local communication - A single button calling a single method? Just use UnityEvents or direct references
- Performance is THE constraint - Building a physics engine or ECS with millions of events/frame? Raw delegates are faster
- Team is unfamiliar - Learning curve exists; if the team isn't on board, it won't be used correctly
- You need synchronous return values - DxMessaging is fire-and-forget; if you need bidirectional request/response, consider other patterns
- Existing large codebase - Migrate incrementally: start with new features, refactor old code gradually (see Migration Guide)
- Small team learning - Try it for one system (e.g., achievements) before going all-in
- Mid-size projects (5-20k lines) - Evaluate after trying it for one complex interaction (e.g., combat or scene transitions)
flowchart TD
Q1{Does your project have 3+<br/>systems that need to talk to each other?}
Q1 -->|NO| A1[Stick with C# events or direct references]
Q1 -->|YES| Q2
Q2{Are you okay with a small<br/>upfront learning investment?}
Q2 -->|NO| A2[Stick with what you know]
Q2 -->|YES| Q3
Q3{Do you need observable, decoupled,<br/>lifecycle-safe messaging?}
Q3 -->|YES| A3["β
Use DxMessaging"]
Q3 -->|NO| A4["β Keep it simple"]
Rule of thumb: If you're reading this README and thinking "this could address several challenges I'm facing," then DxMessaging may be a good fit. If you're thinking "this seems complicated," start with the Visual Guide or stick with simpler patterns.
Looking for hard numbers? See OS-specific Performance Benchmarks.
You write this innocent-looking code:
public class GameUI : MonoBehaviour {
void OnEnable() {
GameManager.Instance.OnScoreChanged += UpdateScore;
}
// Oops, forgot OnDisable... leak! π
}Months later: "Why is our game using 2GB of RAM after an hour?"
public class GameUI : MonoBehaviour {
[SerializeField] private Player player;
[SerializeField] private EnemySpawner spawner;
[SerializeField] private InventorySystem inventory;
[SerializeField] private QuestSystem quests;
[SerializeField] private AudioManager audio;
// ... 15 more SerializeFields ...
void Awake() {
player.OnHealthChanged += UpdateHealth;
spawner.OnWaveStart += ShowWave;
inventory.OnItemAdded += RefreshInventory;
quests.OnQuestCompleted += ShowQuestNotification;
// ... 20 more subscriptions ...
}
}Your UI now depends on many systems. Refactoring becomes more difficult.
Player reports: "My health bar didn't update!"
You think: "Okay, which of the 47 events touching health failed? And in what order?"
30 minutes later: Still setting breakpoints everywhere...
Some developers encounter these challenges when working with traditional event systems:
- Memory leaks from forgotten unsubscribes
- Tight coupling making refactoring difficult
- No execution order control leading to unpredictable behavior
- Limited debugging visibility for tracking message flow
- Boilerplate code when managing many event subscriptions
public class GameUI : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterUntargeted<ScoreChanged>(UpdateScore);
}
// No manual cleanup needed.
// Token automatically handles OnEnable/OnDisable/OnDestroy
}public class GameUI : MessageAwareComponent {
// Zero SerializeFields! Zero references!
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterUntargeted<HealthChanged>(OnHealth);
_ = Token.RegisterUntargeted<WaveStarted>(OnWave);
_ = Token.RegisterUntargeted<ItemAdded>(OnItem);
// Listen to anything, from anywhere, no coupling
}
}Your UI is now independent. Swapping systems no longer requires updating UI references.
Open any MessageAwareComponent in the Inspector:
Message History (last 50):
[12:34:56] HealthChanged (amount: 25) β Priority: 0
[12:34:55] ItemAdded (id: 42, count: 1) β Priority: 5
[12:34:54] WaveStarted (wave: 3) β Priority: 0
Active Registrations:
β HealthChanged (5 handlers)
β ItemAdded (2 handlers)
See exactly what fired, when, and who handled it. No guesswork.
// 1. Define messages (typed, discoverable)
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct Heal { public readonly int amount; }
// 2. Listen (automatic lifecycle - prevents leaks)
public class Player : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterComponentTargeted<Heal>(this, OnHeal);
}
void OnHeal(ref Heal m) {
health += m.amount;
Debug.Log($"Healed {m.amount}!");
}
}
// 3. Send (from anywhere - zero coupling)
var heal = new Heal(50);
heal.EmitComponentTargeted(playerComponent);- β Automatic cleanup - tokens clean up when components are destroyed
- β Zero coupling - no SerializeFields, no GetComponent, no direct references
- β Full visibility - see message flow in Inspector with timestamps and payloads
- β Predictable order - priority-based execution (no more mystery race conditions)
- β Type-safe - compile-time guarantees, refactor with confidence
- β Intercept & validate - enforce rules before handlers run (clamp damage, block invalid input)
- β Extension points everywhere - interceptors, handlers, post-processors with priorities
What DxMessaging offers:
The problem with normal events: Boxing allocations, GC spikes, memory leaks from forgotten unsubscribes.
void OnDamage(ref TookDamage msg) { // Pass by ref = zero allocations
health -= msg.amount; // No boxing, no GC pressure
}
// Automatic cleanup prevents common leak patternsNote: Struct messages passed by ref avoid GC allocations, which is standard behavior for value types in C#.
Most event systems force you into one pattern. DxMessaging gives you the right tool for each job:
// Untargeted: "Everyone, listen up!" (global announcements)
[DxUntargetedMessage]
public struct GamePaused { }
// β³ Perfect for: settings, scene transitions, global state
// Targeted: "Hey Player, do this!" (commands to specific entities)
[DxTargetedMessage]
public struct Heal { public int amount; }
// β³ Perfect for: UI actions, direct commands, player input
// Broadcast: "I took damage!" (events others can observe)
[DxBroadcastMessage]
public struct TookDamage { public int amount; }
// β³ Perfect for: achievements, analytics, UI updates from entitiesWhy this matters: You're not forcing everything through one generic "Event" pattern. Each message type has clear semantics.
Every message flows through 3 stages with priority control:
flowchart LR
P[Producer] --> I[Interceptors<br/>validate/mutate]
I --> H[Handlers<br/>main logic]
H --> PP[Post-Processors<br/>analytics/logging]
The problem with normal events: To track all player damage, enemy damage, and NPC damage, you need 3 separate event subscriptions.
DxMessaging approach: Subscribe once to a message type, receive all instances with source information:
// Track ALL damage from ANY source (players, enemies, NPCs, environment)
_ = token.RegisterBroadcastWithoutSource<TookDamage>(
(InstanceId source, TookDamage msg) => {
Debug.Log($"{source} took {msg.amount} damage!");
Analytics.LogDamage(source, msg.amount);
CheckAchievements(source, msg.amount);
}
);- Achievement system: Track all kills, deaths, damage across the entire game
- Combat log: "Player took 25 damage, Enemy3 took 50 damage, Boss took 100 damage"
- Analytics: Aggregate stats from all entities without knowing about them upfront
- Debug tools: Watch ALL messages in the Inspector without instrumenting code
How this differs: Some event bus patterns require subscribing to each entity type separately. DxMessaging allows observing all instances of a message type in one registration.
The problem with normal events: Validation logic duplicated in every handler, or bugs when you forget.
DxMessaging solution: Validate ONCE before ANY handler runs:
// ONE interceptor protects ALL handlers
_ = token.RegisterBroadcastInterceptor<TookDamage>(
(ref InstanceId src, ref TookDamage msg) => {
if (msg.amount <= 0) return false; // Cancel invalid
if (msg.amount > 999) {
msg = new TookDamage(999); // Clamp excessive
}
if (IsGodModeActive(src)) return false; // Block damage
return true;
},
priority: -100 // Run FIRST
);
// Now ALL handlers receive clean, validated data
_ = token.RegisterComponentTargeted<TookDamage>(player, OnDamage);
void OnDamage(ref TookDamage msg) {
// No validation needed - interceptor guarantees validity!
health -= msg.amount;
}- Clamp/normalize values (damage, healing, speeds)
- Enforce game rules ("can't heal above max health")
- Block messages during cutscenes
- Log/audit sensitive actions
The problem with normal events: "Which event fired? When? Who handled it? In what order?" = π€·
DxMessaging solution: Click any MessageAwareComponent in the Inspector:
[12:34:56.123] HealthChanged- amount: 25
- priority: 0
- handlers: 3
[12:34:55.987] ItemAdded- itemId: 42, count: 1
- priority: 5
- handlers: 2
- β HealthChanged (priority: 0, called: 847 times)
- β ItemAdded (priority: 5, called: 23 times)
- β TookDamage (priority: 10, called: 1,203 times)
- "Did my message fire?" β Check history, see timestamp
- "Why didn't my handler run?" β Check registrations, see if it's active
- "What's firing too often?" β Sort by call count
- "What's the execution order?" β Sort by priority
No more: Setting 50 breakpoints and stepping through code for 30 minutes.
The problem with normal events: Global static events contaminate tests. Mock complexity. Flaky tests.
DxMessaging solution: Each test gets its own isolated message bus:
[Test]
public void TestAchievementSystem() {
// Create isolated bus - zero global state
var testBus = new MessageBus();
var handler = new MessageHandler(new InstanceId(1), testBus) { active = true };
var token = MessageRegistrationToken.Create(handler, testBus);
// Test in isolation
_ = token.RegisterBroadcastWithoutSource<EnemyKilled>(achievements.OnKill);
var msg = new EnemyKilled("Boss");
msg.EmitGameObjectBroadcast(enemy, testBus); // Only this test sees it
Assert.IsTrue(achievements.Unlocked("BossSlayer"));
}
// Bus destroyed, zero cleanup needed- Tests don't interfere with each other
- No "arrange/act/cleanup" boilerplate
- No mocking frameworks needed
- Parallel test execution is supported
- π Documentation Site - Full searchable documentation
- π Wiki - Quick reference wiki
- π Changelog - Version history
- New here? Start with Getting Started Guide (10 min read)
- Want patterns? See Common Patterns
- Deep dive? Read Design & Architecture
- Overview β What and why
- Quick Start β First message in 5 minutes
- Message Types β When to use Untargeted/Targeted/Broadcast
- Interceptors & Ordering β Control execution flow
- Listening Patterns β All the ways to receive messages
- Unity Integration β MessagingComponent deep dive
- Targeting & Context β GameObject vs Component
- Diagnostics β Inspector tools and debugging
Important: Inheritance with MessageAwareComponent
- If you override lifecycle or registration hooks, call the base method.
- Use
base.RegisterMessageHandlers()to keep default stringβmessage registrations. - Use
base.OnEnable()/base.OnDisable()to preserve token enable/disable. - If you need to opt out of string demos, override
RegisterForStringMessages => falseinstead of skipping the base call. - Donβt hide Unity methods with
new(e.g.,new void OnEnable()); alwaysoverrideand callbase.*.
DxMessaging works standalone (zero dependencies) or with any major DI framework. For detailed setup guides and code examples:
- Zenject Integration Guide β Full-featured DI with extensive Unity support
- VContainer Integration Guide β Lightweight DI with scoped lifetimes for scene isolation
- Reflex Integration Guide β Minimal API, high-performance DI
- Runtime Configuration β Setting and overriding message buses at runtime, re-binding registrations
- Message Bus Providers β Provider system and MessageBusProviderHandle for flexible bus configuration
Each guide includes:
- β Complete setup instructions with installers
- β Multiple usage patterns (plain classes, MonoBehaviours, direct injection)
- β Testing examples with isolated buses
- β Advanced patterns (pooling, scene scopes, signal bridges)
See the π§ DI Compatible section above for a quick overview.
- Compare with Other Unity Messaging Frameworks β In-depth comparison with UniRx, MessagePipe, Zenject Signals, C# events, UnityEvents, and more
- Scriptable Object Architecture (SOA) Compatibility β Migration patterns and interoperability with SOA
| Framework | Best For | Key Strength | Unity Support | Learning Curve |
|---|---|---|---|---|
| DxMessaging | Unity pub/sub with lifecycle mgmt | Inspector debugging + control | β Built for Unity | βββ |
| UniRx | Complex event stream transforms | Reactive operators (LINQ) | β Built for Unity | ββ |
| MessagePipe | High-performance DI messaging | Highest throughput (97M ops/sec) | β Built for Unity | ββββ |
| Zenject Signals | DI-integrated messaging | Zenject ecosystem | β Built for Unity | ββ |
| C# Events | Simple, local communication | Minimal overhead | β Native C# | βββββ |
- Unity-first design with GameObject/Component targeting
- Automatic lifecycle management (prevents common memory leaks)
- Inspector debugging to see message flow and history
- Execution order control (priority-based handlers)
- Message validation/interception pipeline
- Global observers (listen to all message instances)
- Post-processing stage (analytics, logging after handlers)
- No dependencies, plug-and-play setup
See full comparison for detailed analysis with code examples, performance benchmarks, and decision guides.
π¦ Using Scriptable Object Architecture (SOA)?
DxMessaging can work alongside or replace SOA patterns. See SOA Compatibility Guide for:
- Fair comparison of SOA vs. DxMessaging
- Migration patterns from GameEvent/FloatVariable to DxMessaging
- How to use both systems together (SOs for configs, DxMessaging for events)
- When to keep using ScriptableObjects (immutable design data)
- Install Guide β All install options (OpenUPM, Git URL, scoped registry, tarball)
- Glossary β All terms explained in plain English
- Quick Reference β Cheat sheet
- API Reference β Complete API
- Helpers β Source generators and utilities
- FAQ β Common questions
- Troubleshooting
Browse all docs: Documentation Hub
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct SceneTransition {
public readonly string sceneName;
}
// Multiple systems react independently
public class AudioSystem : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterUntargeted<SceneTransition>(OnScene, priority: 0);
}
void OnScene(ref SceneTransition m) => FadeOutMusic();
}
public class SaveSystem : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterUntargeted<SceneTransition>(OnScene, priority: 0);
}
void OnScene(ref SceneTransition m) => SaveGame();
}// Listen to ALL events for achievement tracking
public class AchievementTracker : MessageAwareComponent {
protected override void RegisterMessageHandlers() {
base.RegisterMessageHandlers();
_ = Token.RegisterGlobalAcceptAll(
acceptAllUntargeted: m => Check(m),
acceptAllTargeted: (t, m) => Check(m),
acceptAllBroadcast: (s, m) => Check(m)
);
}
}- Zero GC allocations for struct messages
- ~10ns overhead per handler (compared to C# events)
- Type-indexed caching for O(1) lookups
- Optimized for hot paths with aggressive inlining
See Design & Architecture for details.
For OS-specific benchmark tables generated by PlayMode tests, see Performance Benchmarks.
| Feature | DxMessaging | UniRx | MessagePipe | Zenject Signals |
|---|---|---|---|---|
| Unity Compatibility | β Built for Unity | β Built for Unity | β Built for Unity | β Built for Unity |
| Decoupling | β Full | β Full | β Full | β Full |
| Lifecycle Safety | β Auto | |||
| Execution Order | β Priority | β None | β None | β None |
| Type Safety | β Strong | β Strong | β Strong | β Strong |
| Inspector Debug | β Built-in | β No | β No | β No |
| GameObject Targeting | β Yes | β No | β No | β No |
| Global Observers | β Yes | β No | β No | β No |
| Interceptors | β Pipeline | β No | β No | |
| Post-Processing | β Dedicated | β No | β No | |
| Stream Operators | β No | β Extensive | β No | |
| Performance | β Good (10-17M) | β Good (18M) | β High (97M) | |
| Dependencies | β None | β None |
| Feature | DxMessaging | C# Events | UnityEvents | Static Event Bus |
|---|---|---|---|---|
| Decoupling | β Full | β Tight | β Yes | |
| Lifecycle Safety | β Auto | β Manual | β Manual | |
| Execution Order | β Priority | β Undefined | β Undefined | β Undefined |
| Type Safety | β Strong | β Strong | ||
| Context (Who/What) | β Rich | β None | β None | β None |
| Interception | β Yes | β No | β No | β No |
| Observability | β Built-in | β No | β No | β No |
| Performance | β Zero-alloc | β Good | β Good |
Import samples from Package Manager:
- Mini Combat β Simple combat with Heal/Damage messages
- UI Buttons + Inspector β Interactive diagnostics demo
- Unity 2021.3 or later
- .NET Standard 2.1
- Works with all render pipelines (URP, HDRP, Built-in)
See Compatibility for details.
Contributions welcome! See Contributing.
MIT License - see License
Created and maintained by wallstop studios
- π¦ Package on GitHub
- π Report Issues
- π Documentation Site
- π Wiki