Refactor: NdGameSdk Module System (EntryPoint + Submodules)
Summary
Refactor the NdGameSdk module model so the SDK can:
- Run without any module: only
InitializeSdk(cfg) is called; base patches/components are active.
- Optionally host one Main EntryPoint module (e.g.,
Nd.modclient) and any number of Submodules registered by that EntryPoint.
This gives developers two paths:
- Build a custom EntryPoint (strict/specific purpose of modding).
- Build submodules that extend the approved EntryPoint (
Nd.modclient).
Scope / Goals
- Allow
InitializeSdk(&cfg) to succeed with no module.
- Enforce single EntryPoint (a.k.a. “Main module”).
- Add Submodule concept (+ register/unregister).
- Both EntryPoint and Submodules can use SDK components & events.
- EntryPoint can discover/track submodules and optionally expose internal services to them (via a small “services bridge”).
- Provide soft standards + a patching utility component to avoid chaos.
Non-goals
- Changing core SDK component lifecycle.
Public API
New/Updated
InitializeSdk(const SdkConfig* cfg)
RegisterEntryPoint(ISdkModule* main, EntryPointServices* services /*nullable*/)
UnregisterEntryPoint(ISdkModule* main)
RegisterSubmodule(ISdkModule* sub)
UnregisterSubmodule(ISdkModule* sub)
GetEntryPoint() -> ISdkModule* /*nullable*/
GetSubmodules() -> std::vector<ISdkModule*>
HasEntryPoint() -> bool
enum class SdkModuleKind { EntryPoint, Submodule }
struct EntryPointServices { virtual ~EntryPointServices() = default; /* opaque bridge */ }
Deprecated
RegisterSdkModule(ISdkModule* main) → alias to RegisterEntryPoint.
ISdkModule (lifecycle hooks)
OnModuleRegistered()
OnRoleAssigned(SdkModuleKind role, ISdkModule* entrypoint, EntryPointServices* services)
EntryPoint: role=EntryPoint, entrypoint=this
Submodule: role=Submodule, entrypoint=GetEntryPoint()
- (EntryPoint only)
OnSubmoduleRegistered(ISdkModule* sub), OnSubmoduleUnregistered(ISdkModule* sub)
Errors (extend SdkModuleException)
AlreadyHasEntryPoint
NoEntryPoint
InvalidRole
Runtime Rules
- Base-only mode: only
InitializeSdk. No modules registered. SDK logs that it runs without an EntryPoint; components still function.
- EntryPoint: can be registered once. Second registration →
AlreadyHasEntryPoint.
- Submodules: require an active EntryPoint; otherwise →
NoEntryPoint.
- Wiring: EntryPoint and Submodules receive the same SDK component events.
- Order on register:
OnModuleRegistered() → OnRoleAssigned(...).
EntryPoint also receives OnSubmoduleRegistered/Unregistered.
- Lifetime: SDK does not own module objects. On EntryPoint unload, SDK auto-unregisters submodules first.
- Thread-safety: operations guarded by a registry mutex.
Soft Standards: Patching Utilities (SDK Component)
Try to Introduce a PatchManager SDK component to standardize patching:
- Capabilities (conceptual):
- Detours (store by name/id), branch/call patches, byte patches with restore.
- Grouping/scope (remove by group); auto-cleanup on module unload.
- Address resolution goes through SDK discovery (patterns/symbols), no raw magic offsets in modules.
- Naming convention:
"<ModuleName>/<System>/<Action>" .
- Logging: success/failure with name + address; verify page protection & instruction boundaries.
Access via GetSharedSdkComponent<PatchManager>().
EntryPoint Services Bridge
- Minimal
EntryPointServices interface acts as an opaque bridge from EntryPoint to Submodules.
- The EntryPoint (e.g.,
Nd.modclient) implements a derived services class to expose optional internals (e.g., NdMods accessor) that submodules can consume after OnRoleAssigned.
Migration Notes
- Base-only: just call
InitializeSdk(&cfg); skip registration.
- Existing main module: replace
RegisterSdkModule(main) with RegisterEntryPoint(main, &services /*optional*/); optionally handle submodule callbacks.
- Submodules: implement
ISdkModule, rely on EntryPoint to call RegisterSubmodule(this); use OnRoleAssigned to bind services and SDK components.
Refactor: NdGameSdk Module System (EntryPoint + Submodules)
Summary
Refactor the NdGameSdk module model so the SDK can:
InitializeSdk(cfg)is called; base patches/components are active.Nd.modclient) and any number of Submodules registered by that EntryPoint.This gives developers two paths:
Nd.modclient).Scope / Goals
InitializeSdk(&cfg)to succeed with no module.Non-goals
Public API
New/Updated
InitializeSdk(const SdkConfig* cfg)RegisterEntryPoint(ISdkModule* main, EntryPointServices* services /*nullable*/)UnregisterEntryPoint(ISdkModule* main)RegisterSubmodule(ISdkModule* sub)UnregisterSubmodule(ISdkModule* sub)GetEntryPoint() -> ISdkModule* /*nullable*/GetSubmodules() -> std::vector<ISdkModule*>HasEntryPoint() -> boolenum class SdkModuleKind { EntryPoint, Submodule }struct EntryPointServices { virtual ~EntryPointServices() = default; /* opaque bridge */ }Deprecated
RegisterSdkModule(ISdkModule* main)→ alias toRegisterEntryPoint.ISdkModule (lifecycle hooks)
OnModuleRegistered()OnRoleAssigned(SdkModuleKind role, ISdkModule* entrypoint, EntryPointServices* services)EntryPoint:
role=EntryPoint, entrypoint=thisSubmodule:
role=Submodule, entrypoint=GetEntryPoint()OnSubmoduleRegistered(ISdkModule* sub),OnSubmoduleUnregistered(ISdkModule* sub)Errors (extend
SdkModuleException)AlreadyHasEntryPointNoEntryPointInvalidRoleRuntime Rules
InitializeSdk. No modules registered. SDK logs that it runs without an EntryPoint; components still function.AlreadyHasEntryPoint.NoEntryPoint.OnModuleRegistered()→OnRoleAssigned(...).EntryPoint also receives
OnSubmoduleRegistered/Unregistered.Soft Standards: Patching Utilities (SDK Component)
Try to Introduce a
PatchManagerSDK component to standardize patching:"<ModuleName>/<System>/<Action>".EntryPoint Services Bridge
EntryPointServicesinterface acts as an opaque bridge from EntryPoint to Submodules.Nd.modclient) implements a derived services class to expose optional internals (e.g.,NdModsaccessor) that submodules can consume afterOnRoleAssigned.Migration Notes
InitializeSdk(&cfg); skip registration.RegisterSdkModule(main)withRegisterEntryPoint(main, &services /*optional*/); optionally handle submodule callbacks.ISdkModule, rely on EntryPoint to callRegisterSubmodule(this); useOnRoleAssignedto bind services and SDK components.