Skip to content

JWalkerMailly/exBase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

30 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Warning

exBase is currently in open beta. If you encounter issues, unexpected behaviour, or have questions, please open an issue.

exBase lets you write GM hooks directly inside your weapon while automatically handling: hook registration and cleanup, active weapon checks and per-instance dispatching. No more hook lifecycle management, no more duplicated logic.

exBase acts as a hook abstraction layer rather than a traditional weapon base. It can be used as the base of a weapon, as the base for a weapon base, or integrated directly via its module, though more setup is required.

Get it on the workshop: exBase - Extended Weapon Base

Architecture features:

  • Zero-boilerplate
  • Single proxy per event
  • O(1) dispatch time
  • Hot-reload support
  • Weak key GC safety
  • Idempotent registration / cleanup
  • Guaranteed cleanup fallback path
  • No per-dispatch iteration, string operations, or allocations

πŸ“‘ Table of contents


⚑ Quick start

Create a weapon and inherit from weapon_exbase. Do not override Initialize, OnRestore, OnRemove or OnReloaded (see Getting started why).

SWEP.Base = "weapon_exbase"

...

function SWEP:PlayerButtonDown(ply, button)
	-- your code
end

Done. Your SWEP can now use GM:PlayerButtonDown and exBase automatically handles registration, routing and cleanup.


🧭 Getting started

Using exBase as a weapon base (recommended)

Inherit from weapon_exbase and use the provided convenience functions. Do not override Initialize, OnRestore, OnRemove or OnReloaded, exBase uses these to manage hook lifecycles.

You can also download templates here.

SWEP.Base      = "weapon_exbase"
SWEP.Author    = "..."
SWEP.Spawnable = true

...

function SWEP:CustomInitialize()
	-- your initialize code here.
end

function SWEP:CustomOnRestore()
	-- your onrestore code here.
end

function SWEP:CustomOnRemove()
	-- your onremove code here.
end

function SWEP:CustomOnReloaded()
	-- your onreloaded code here.
end

Tip

If you need to override Initialize, OnRestore, OnRemove or OnReloaded, make sure to call the corresponding function on BaseClass so exBase can preserve its lifecycle management.

For more information, see baseclass.Get.


Integrating exBase directly into your weapon base

If you wish to implement exBase behaviour without inheriting from it, you can do so using the following template:

Warning

Only use this path if you have an existing weapon base you cannot change. Requires manual lifecycle calls.

require("exbase_registry")

SWEP.Base      = "weapon_base"
SWEP.Author    = "..."
SWEP.Spawnable = true

...

function SWEP:Initialize() -- mandatory
	exbase_registry.Register(self)
end

function SWEP:OnRestore()  -- mandatory
	exbase_registry.Register(self)
end

function SWEP:OnRemove()   -- mandatory
	exbase_registry.Deregister(self)
end

function SWEP:OnReloaded() -- recommended
	exbase_registry.Register(self, true)
end

You are now free to implement custom callbacks for Initialize, OnRestore, OnRemove and OnReloaded as you see fit. You may also manage hook lifecycles in another part of your code, but this is the recommended way.


πŸ“– How to use

Now that you are inheriting from exBase, you are free to use any GM hook in your weapon as long as the following conditions are met:

  • The hook's signature contains a Player argument (server-side). If it does not provide a Player argument but provides a generic Entity argument instead, please see Example 2.
  • The hook is client-side (fallback to LocalPlayer() if no Player argument is found).

Example 1

When weapon is active, the user takes no fall damage:

SWEP.Base      = "weapon_exbase"
SWEP.Author    = "..."
SWEP.Spawnable = true

...

if (SERVER) then

	function SWEP:GetFallDamage(ply, speed)
		return 0
	end

end

That's all, the weapon uses GM:GetFallDamage and exBase proxies the hook while managing its lifecycle.


Example 2 - hooks without a Player argument

Some hooks like GM:EntityTakeDamage are server-side and do not provide a Player object, rather, they provide a generic Entity. Using exbase_indexer.Hint(eventName, plyIndex) as a decorator, we can specify which argument index the Player object will be found at.

In this example, the user takes half damage when the weapon is active:

SWEP.Base      = "weapon_exbase"
SWEP.Author    = "..."
SWEP.Spawnable = true

...

if (SERVER) then

	exbase_indexer.Hint("EntityTakeDamage", 1) -- Entity at index 1 (target) is the player
	function SWEP:EntityTakeDamage(target, dmg)
		dmg:ScaleDamage(0.5)
	end

end

Caution

This is required for any server-side hook that has no Player object in its signature. Skipping this will lead to inconsistent behaviour, or skipped hook calls.


Example 3

When the weapon is active, a cartoon shader is applied:

SWEP.Base      = "weapon_exbase"
SWEP.Author    = "..."
SWEP.Spawnable = true

...

if (CLIENT) then

	local mod = {
		["$pp_colour_addr"]       = 0,
		["$pp_colour_addg"]       = 0,
		["$pp_colour_addb"]       = 0,
		["$pp_colour_brightness"] = -0.04,
		["$pp_colour_contrast"]   = 1.35,
		["$pp_colour_colour"]     = 5,
		["$pp_colour_mulr"]       = 0,
		["$pp_colour_mulg"]       = 0,
		["$pp_colour_mulb"]       = 0
	}

	function SWEP:RenderScreenspaceEffects()
		DrawColorModify(mod)
		DrawSobel(0.5)
	end

end

Here, the weapon makes use of a generic client-side hook GM:RenderScreenspaceEffects with no Player argument. In this case, exBase will recognize the intent and default to LocalPlayer() as its argument proxy during dispatching.


πŸ” Debugging

Module level debug concommands are provided for you to inspect all caches.

cl.exbase_hooks / sv.exbase_hooks

Prints registered hooks per class and class reference count.

example:

] sv.exbase_hooks 

Classes [class] | [hooks]:
["weapon_exbase_test"]:
		["EntityTakeDamage"] = function: 0xcfbf0be2
		["GetFallDamage"]    = function: 0xcaddeca2

Class Instances Count [class]  | count:
["weapon_exbase_test"] = 1


cl.exbase_indexer / sv.exbase_indexer

Prints indexer cache of resolved player indices.

example:

] sv.exbase_indexer 

Resolved Ply Indices [hook] | plyIndex:
["EntityTakeDamage"]        = 1


cl.exbase_proxy / sv.exbase_proxy

Prints cache of hooks currently being proxied.

example:

] sv.exbase_proxy 

Proxied Hooks [hook] | isProxied:
["EntityTakeDamage"] = true
["GetFallDamage"]    = true


cl.exbase_registry / sv.exbase_registry

Prints registry cache. The cache presents which weapon method is delegated through proxied hooks, cached class for each weapon instance and hook ref-counts.

example:

] sv.exbase_registry 

Delegates [weapon] | [proxiedHooks]:
[Weapon [76][weapon_exbase_test]]:
		["EntityTakeDamage"] = function: 0xcfbf0be2
		["GetFallDamage"]    = function: 0xcaddeca2

Classes [weapon] | "class":
[Weapon [76][weapon_exbase_test]] = weapon_exbase_test

Hook Counts: [hook]  | count
["EntityTakeDamage"] = 1
["GetFallDamage"]    = 1


πŸ—οΈ How it works

One proxy per event, not per instance
When the first instance of a weapon is created, exBase registers a single hook.Add proxy for each hookable methods defined for that class. Every subsequent instance of the same class shares that proxy, there is no accumulation of hooks as more weapons are created. The proxy is ref-counted and removed via hook.Remove only when the last instance is destroyed.

Class-level scanning, not instance-level
The weapon class table is scanned exactly once per class per lifetime. The scan iterates a pre-filtered table of valid GM hooks and performs a single key lookup into the weapon table per entry. The result is cached and reused by every subsequent instance of that class at zero scan cost.

Shared function references, not copies
The functions stored in the delegate cache are references to the class-level method, not copies, not closures. All instances of the same class share one function reference per hook. The correct self is injected at dispatch time, so each instance operates on its own entity despite sharing the function. This means the memory footprint of the delegate cache scales with the number of distinct weapon classes, not the number of live instances.

Permanent index resolution
When a GM hook fires, exBase must identify which argument holds the player in order to retrieve their active weapon. This resolution is lazy on first call and cached for the duration of the session. GM hook signatures are fixed by the engine and never change at runtime. All future dispatches skip resolution and read the index from cache in O(1).

For hooks with no player argument (server-side hooks like EntityTakeDamage), the index must be provided via exbase_indexer.Hint, which pre-populates the cache before any instance exists, bypassing the resolution step entirely.

Dispatch cost per hook call
Once the system is warm, a single dispatch looks like:

  • One table lookup to resolve the player index
  • One GetActiveWeapon call (cached locally)
  • One table lookup to check the weapon tag (for validity)
  • One table lookup to retrieve the delegate
  • One function call with self injected

No per-dispatch iteration, string operations, or allocations.

Cleanup is guaranteed
SWEP:OnRemove handles the clean deregistration path. An EntityRemoved hook runs as a fallback for edge cases where OnRemove is not called. Both paths converge on the same Deregister function which is idempotent. Calling it twice is safe. Furthermore, some caches implement weak keys to avoid leaks when weapon instances 'disappear'.

01 - Load Time (once per server start)

ENTRY weapon_exbase.lua
SWEP base file is loaded. AddCSLuaFile() chain is resolved from require and sent to all clients.

↓

REQUIRE exbase_registry
Daisy-chains exbase_proxy, exbase_indexer and exbase_hooks.

↓

INIT exbase_hooks
Loads gamemode_hooks.lua and pre-filters it, removing any key also present in the Weapon metatable or in weapon_hooks.lua. Avoids collisions like GM:CalcViewModelView vs WEAPON:CalcViewModelView.

↓

RESULT _hookTable
A pre-filtered, static set of proxiable GM hooks ready for use.


02 - First instance of a weapon class

TRIGGER SWEP:Initialize()
Called by engine, base calls to exbase_registry.Register(self).

↓

REGISTRY exbase_hooks.Acquire(class)
If the class is not cached yet, call scan(class). Iterate _hookTable to find proxiable delegates.

↓

RESULT hooks table cached
Class cache of exbase_hooks is populated with { [event] = delegate }. Reference _count is incremented, skipping scan(class) on subsequent instances.

↓

PROXY exbase_proxy.Register()
For each proxiable hooks, registers a single hook.Add if one doesn't exist. Proxies are ref-counted so only one proxy per event regardless of instance count.


03 - Hook fires at runtime (dispatch)

GMOD GM hook called
exbase_proxy intercepts via hook.Add, e.g. GetFallDamage, RenderScreenspaceEffects.

↓

INDEXER exbase_indexer.Resolve()
If player index is found in cache, return stored index. If not found, Resolve(event, args) for first valid Player object and add index to cache.

↓

LOOKUP GetActiveWeapon
Use cached version of PLAYER:GetActiveWeapon in order to remain lean and confirm weapon uses exBase through weapon[_tag].

↓

DISPATCH delegate(weapon, ...)
Weapon's method is called, passing itself as the first argument for colon syntax support, e.g. SWEP:GetFallDamage, followed by original hook arguments.

↓

Note

GUARD SERVER + no player argument?
no argument: return and drop event
has argument: continue and dispatch

Note

GUARD CLIENT + spectating
ply != LocalPlayer(): return and drop event
ply == LocalPlayer(): continue and dispatch

Note

GUARD Weapon validity
invalid or no tag: return and drop event
valid and tagged: continue and dispatch


04 - Playerless hook override (hint)

WEAPON FILE exbase_indexer.Hint(event, i)
Called at file scope. Writes directly into indexer cache before any instance exists. Acts as a decorator annotation for the method defined below it.
e.g. exbase_indexer.Hint("EntityTakeDamage", 1), no player in signature, index provided manually.

↓

RESULT Cache pre-populated
Resolve() will find the index immediately on first call, no scan attempted. Cache is permanent, GM hook signatures are static in nature.

↓

DISPATCH SERVER guard bypass
With a valid Player index in cache, the dispatch proceeds normally to the weapon's delegate.


05 - Weapon removal and cleanup

PRIMARY PATH SWEP:OnRemove()
Called by engine, invokes exbase_registry.Deregister(self).

Warning

FALLBACK PATH EntityRemoved hook
Catches cases where OnRemove was not called, for example, during a map cleanup edge case or fullupdate events.

↓

RESULT unregister(weapon)
Decrements per-event reference counts. When a count hits zero, calls exbase_proxy.Deregister(event) which then calls hook.Remove.

Warning

FALLBACK RESULT Deregister(ent)
Same deregister path as primary. Guarded to make it idempotent.

↓

CLEANUP exbase_hooks.Release(class)
Decrements class reference count. Evicts class cache when it reaches zero, allowing for re-scan on next instantiation.

Warning

FALLBACK CLEANUP
Guarded, ensures the fallback is not ran if the primary path OnRemove ran cleanly.


⚠️ Disclaimer

Please do not repackage exBase in your own addons. Add the workshop release as dependency to make sure everyone uses the same base and stays up to date. Repackaging leads to mutations which leads to inevitable conflicts.

About

exBase is a lean weapon base for Garry's Mod that allows weapons to participate in GM Hooks with zero boilerplate

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages