Understanding the component proxy system that powers Mythic Framework
The proxy pattern is the core architectural pattern of Mythic Framework. It enables modular, extensible development through a component registration and dependency injection system. Understanding this pattern is essential for working with Mythic.
Reference Implementation:MythicFramework/resources/[mythic]/mythic-base/core/sh_proxy.luaThis file contains the complete proxy system implementation.
At the heart of the proxy system is the global COMPONENTS table:
Copy
-- Global table accessible from all resourcesCOMPONENTS = {}-- Example of what it contains after initialization:COMPONENTS = { Proxy = {...}, -- The proxy system itself Logger = {...}, -- Logging component Database = {...}, -- Database operations Inventory = {...}, -- Inventory management Characters = {...}, -- Character system Jobs = {...}, -- Job system -- ... 60+ more components}
Every registered component becomes available in this global table.
component_name (string) - Unique name for the component
component_data (table) - The component object with methods and data
Example:
Copy
-- In mythic-inventory/server/component.luaexports['mythic-base']:RegisterComponent('Inventory', { -- Component methods Get = function(self, characterId, callback) -- Fetch inventory from database COMPONENTS.Database.Game:findOne({ collection = 'inventory', query = { owner = characterId } }, function(success, inventory) if callback then callback(success, inventory) end end) end, AddItem = function(self, characterId, item, count, metadata, callback) -- Add item logic (async) self:Get(characterId, function(success, inventory) if success and inventory then -- ... add item to inventory if callback then callback(true) end else if callback then callback(false) end end end) end, RemoveItem = function(self, characterId, slot, count) -- Remove item logic return true end, -- Mark as protected to prevent overriding _protected = true, -- Specify required dependencies _required = { 'Get', 'AddItem', 'RemoveItem' }, -- Component name for logging _name = 'inventory'})
Protected Components:Components can be marked as _protected = true to prevent other resources from overriding them:
Copy
-- This component cannot be overriddenCOMPONENTS.Inventory = { _protected = true, Get = function(self) ... end}-- Attempting to register again will fail with warningexports['mythic-base']:RegisterComponent('Inventory', {...})-- Warning: "Attempt To Override Protected Component: Inventory"
Required Attributes:Components can specify _required to enforce that certain methods must exist:
Copy
COMPONENTS.Payment = { _required = { 'Charge', 'Refund' }, Charge = function(self, amount) ... end, Refund = function(self, amount) ... end}-- Extending without required methods will failexports['mythic-base']:ExtendComponent('Payment', { AddBonus = function(self) ... end -- Missing Charge and Refund - will fail})
Purpose: Retrieve a component for useLocation:mythic-base/core/sh_proxy.lua:67
Copy
local component = exports['mythic-base']:FetchComponent(component_name)
Parameters:
component_name (string) - Name of the component to fetch
Returns:
Component object or nil if not found
Example:
Copy
-- Fetch the Inventory componentlocal Inventory = exports['mythic-base']:FetchComponent('Inventory')if Inventory then -- Use the component local items = Inventory:Get(characterId) Inventory:AddItem(characterId, 'water', 5)else print('Inventory component not found!')end
Best Practice:
Copy
-- Fetch once at resource start, reuse throughoutlocal Inventory, Jobs, CharactersAddEventHandler('Core:Shared:Ready', function() Inventory = exports['mythic-base']:FetchComponent('Inventory') Jobs = exports['mythic-base']:FetchComponent('Jobs') Characters = exports['mythic-base']:FetchComponent('Characters')end)-- Now use them anywhereAddEventHandler('myresource:doSomething', function() local items = Inventory:Get(source)end)
Always check if component exists before using! A component may not be loaded yet, or the resource providing it may not be started.
component_name (string) - Name of component to extend
extension_data (table) - Methods/data to add
Example:
Copy
-- Original Inventory componentCOMPONENTS.Inventory = { Get = function(self, charId) ... end, AddItem = function(self, charId, item) ... end}-- Extend with new methodexports['mythic-base']:ExtendComponent('Inventory', { -- Add a new method GetWeight = function(self, charId) local inv = self:Get(charId) local totalWeight = 0 for _, item in ipairs(inv) do totalWeight = totalWeight + (item.weight * item.count) end return totalWeight end, -- Add a new property maxWeight = 100})-- Now you can use the new methodlocal weight = COMPONENTS.Inventory:GetWeight(characterId)
component_name (string) - Name of your component (for logging)
dependencies (table) - Array of component names to wait for
callback (function) - Called when dependencies are ready (or failed)
Example:
Copy
-- mythic-shops/server/main.lua-- Request dependencies before initializingexports['mythic-base']:RequestDependencies('Shops', { 'Inventory', -- Need Inventory component 'Economy', -- Need Economy component 'Logger' -- Need Logger component}, function(errors) if #errors > 0 then print('Failed to load dependencies:', json.encode(errors)) return end -- All dependencies loaded, safe to initialize local Inventory = COMPONENTS.Inventory local Economy = COMPONENTS.Economy local Logger = COMPONENTS.Logger -- Register shop component exports['mythic-base']:RegisterComponent('Shops', { Purchase = function(self, player, item, price) if Economy:Charge(player, price) then Inventory:AddItem(player, item, 1) Logger:Info('Shops', player .. ' purchased ' .. item) return true end return false end }) print('Shops component initialized successfully')end)
How it Works:
Polls for dependencies every 100ms
Times out after 50 attempts (~5 seconds)
Calls callback with any errors
Tracks which components depend on others for updates
Advanced Usage:
Copy
-- Multiple resources depending on same componentexports['mythic-base']:RequestDependencies('Banking', { 'Economy' }, function(errors) if #errors == 0 then COMPONENTS.Economy:RegisterAccount('bank', 'Main Bank') endend)exports['mythic-base']:RequestDependencies('CryptoWallet', { 'Economy' }, function(errors) if #errors == 0 then COMPONENTS.Economy:RegisterAccount('crypto', 'Crypto Wallet') endend)-- When Economy component updates, both Banking and CryptoWallet are notified
-- mythic-garage/server/component.luaexports['mythic-base']:RequestDependencies('Garage', { 'Vehicles', 'Characters', 'Database'}, function(errors) if #errors > 0 then return end exports['mythic-base']:RegisterComponent('Garage', { _protected = true, _name = 'garage', -- Get player's vehicles GetVehicles = function(self, characterId, callback) COMPONENTS.Database.Game:find({ collection = 'vehicles', query = { owner = characterId } }, function(success, vehicles) if callback then callback(success, vehicles) end end) end, -- Spawn vehicle from garage SpawnVehicle = function(self, player, vehicleId, callback) COMPONENTS.Database.Game:findOne({ collection = 'vehicles', query = { _id = vehicleId } }, function(success, vehicle) if not success or not vehicle then if callback then callback(false) end return end -- Check ownership local char = COMPONENTS.Characters:GetCharacter(player) if vehicle.owner ~= char.SID then if callback then callback(false, 'Not your vehicle') end return end -- Spawn logic local coords = GetEntityCoords(GetPlayerPed(player)) local veh = COMPONENTS.Vehicles:SpawnVehicle(vehicle.model, coords, vehicle.customization) if callback then callback(true, veh) end end) end, -- Store vehicle in garage StoreVehicle = function(self, player, vehicleEntity) local plate = GetVehicleNumberPlateText(vehicleEntity) -- Save customization local mods = COMPONENTS.Vehicles:GetVehicleMods(vehicleEntity) COMPONENTS.Database.Game:updateOne({ collection = 'vehicles', query = { plate = plate }, update = { ['$set'] = { customization = mods, stored = true } } }, function(success) if success then -- Delete vehicle after successful save DeleteEntity(vehicleEntity) end end) return true end })end)
-- mythic-inventory-weight/server/main.lua-- Add weight management to existing Inventory componentexports['mythic-base']:RequestDependencies('InventoryWeight', { 'Inventory'}, function(errors) if #errors > 0 then return end -- Store original AddItem local originalAddItem = COMPONENTS.Inventory.AddItem -- Extend with weight checking exports['mythic-base']:ExtendComponent('Inventory', { maxWeight = 100, GetWeight = function(self, charId) local inv = self:Get(charId) local weight = 0 for _, item in pairs(inv) do weight = weight + (item.weight * item.count) end return weight end, AddItem = function(self, charId, item, count, metadata) -- Check weight first local currentWeight = self:GetWeight(charId) local itemWeight = COMPONENTS.Items:GetItemWeight(item) * count if currentWeight + itemWeight > self.maxWeight then return false, 'Inventory too heavy' end -- Call original return originalAddItem(self, charId, item, count, metadata) end })end)
Never assume components are available immediately:
Copy
-- ❌ BADlocal Inventory = COMPONENTS.Inventory -- May be nil!-- ✅ GOODexports['mythic-base']:RequestDependencies('MyResource', { 'Inventory' }, function(errors) if #errors == 0 then local Inventory = COMPONENTS.Inventory -- Guaranteed to exist endend)
exports['mythic-base']:RegisterComponent('Garage', { ---@param characterId number Character ID ---@param vehicleId number Vehicle ID ---@return boolean success ---@return table|string vehicle or error message SpawnVehicle = function(self, characterId, vehicleId) -- implementation end})
-- Component with event subscriptionexports['mythic-base']:RegisterComponent('EventBus', { listeners = {}, On = function(self, event, callback) if not self.listeners[event] then self.listeners[event] = {} end table.insert(self.listeners[event], callback) end, Emit = function(self, event, ...) if self.listeners[event] then for _, callback in ipairs(self.listeners[event]) do callback(...) end end end})