ygopro-handler 0.1.1

A type-erased, plugin-based message handler framework for YGOPro duel rooms.
Documentation

A type-erased, plugin-based message handler framework.

This crate provides the machinery to dispatch messages to registered handlers, extract request data, and combine handler responses. Every incoming message flows through a [Processor] and is handled by a set of handlers keyed by message type.

The extractor/handler pattern mirrors axum: handlers are plain functions whose parameters are pulled from a [Bundle] via [FromRequest], and whose return value is converted into a response via [IntoResponse]. Any type implementing [FromRequest] can be a parameter, and any type implementing [IntoResponse] can be returned — exactly like axum's FromRequest and IntoResponse.

Unlike axum, however, a message is not handled by a single handler but by a chain of handlers (all those registered for the message key, plus the globals), and the chain can be aborted mid-way via [StopFlag]. This makes it easy to layer plugins: each plugin registers its own handlers for a message, and any one of them can stop the chain or replace the message that the rest see.

Design goals

  • Handlers as closures. A handler is any callable that takes extractable parameters and returns a value convertible into a [extract::Response]. Up to 16 parameters are supported; each is pulled from a [Bundle] via [FromRequest].
  • Message key dispatch. Each message carries a [MessageKey] (usually a u8 flag). The [Processor] routes a message to the handlers registered for that key, plus any global handlers.
  • Chained, abortable processing. A message flows through every handler registered for its key, plus the globals, in priority order. Any handler can abort the chain via [StopFlag] or replace the message that downstream handlers see. Their outputs are combined through [std::ops::Mul] on the response, letting one handler replace the message, another swallow it, and another terminate the room.
  • Type-erased handlers. [SyncHandler], [AsyncHandler] and [TowerHandler] erase the concrete handler type so a heterogeneous set of handlers can live in a single [Processor].

Soundness

This crate is unsafe. You must carefully handle the parameters to prevent undefined behaviour, especially dual mutable references — a handler that takes two &mut parameters can alias the same memory. The extraction uses raw-pointer casts because the Rust compiler cannot properly calculate the required lifetimes, and due to the architecture the crate cannot offer the same guarantees that axum's FromRequest provides.

Performance

The crate offers three handler wrappers, trading features against speed:

  1. [TowerHandler] — the most feature-complete, and the slowest. Between the handler and the processor it inserts a tower Service layer (HandlerService), a boxed future (HandlerServiceFuture), and a BoxCloneService, so every call pays for several layers of boxing and a oneshot dispatch.
  2. [AsyncHandler] — gives up the tower adaptation layer. It holds the handler in an Arc<dyn Call> and boxes only the future, so it drops the extra Service boxing while staying cloneable through a cheap Arc clone.
  3. [SyncHandler] — gives up the async nature of [Handler]: its future is always Ready. In exchange it boxes nothing per call, drops the 'static bound on the request, state, and response, and enables the dual-state trick ([handler::sync_handler::WithSubState]).

Compared with the C++ original, which dispatches messages with a direct switch or a virtual call, every handler here pays for at least one heap-allocated future and a type-erasure indirection (a trait object or a function pointer) per invocation. [Processor::process] additionally awaits the whole handler chain per item, moving the [Bundle] through it. For the hot path (game messages) this is one boxed future per handler, which is acceptable at the rate messages are emitted.

Example

A processor dispatches a message to the handlers registered for its key:

use ygopro_handler::Processor;
use ygopro_handler::TowerHandler;
use ygopro_handler::Bundle;
use ygopro_handler::State;
use ygopro_handler::extract::Request;
use ygopro_handler::extract::Response;

type Req = Request<u8, ()>;
type Res = Response<u8>;

let mut processor = Processor::<u8, Req, State, Res>::new();
processor.register(7, TowerHandler::new(0, "test", "example", |message: &u8| -> Res {
    if *message == 7 {
        Res::Replace(*message)
    } else {
        Res::Continue
    }
}));

let bundle = Bundle::new(Request { message: 7, extra: () }, State::new(), Res::Continue);
let result = tokio::runtime::Runtime::new().unwrap().block_on(processor.process_bundle(bundle, 7));
assert!(matches!(result.response, Res::Replace(7)));