Elixer - Cross-Mod Compatible Utility Library for Modders

remiremi remedy [blu.knight] Join Date: 2003-11-18 Member: 23112Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester
edited June 2014 in Modding
Allow me to introduce... Elixer!

https://github.com/sclark39/NS2Elixer

This is my special utility file which I use for replacing local functions, debugging, and general niceties. It is in use already by CompMod and NS2+ and has been for about a month. It has built in functionality to be able to keep more than one version of the library in memory and switch them out on demand, so as long as you put this in a unique path in your mod, it will never conflict with other mods.
-- Function List
Elixer.UseVersion( versionNum )
EPrint( fmt, ... )
EPrintDebug( fmt, ... )
EPrintCallHook( class, name )
Class_AddMethod( className, methodName, method )
upvalues( func )
PrintUpValues( func )
GetUpValue( func, upname, options )
LocateUpValue( func, upname, options )
SetUpValues( func, source )
CopyUpValues( dst, src )
ReplaceUpValue( func, localname, newval, options )
AppendToEnum( tbl, key )
list ( ... )
set( tbl )


To use, throw this at the top of your client, predict, and server files:
Script.Load( "lua/youruniquesubdir/Elixer_Utility.lua" )
Elixer.UseVersion( 1.71 )


Here is a simple example of how to use the ReplaceUpValue function
ReplaceUpValue( GUIMinimap.Update, "UpdateStaticBlips", NewUpdateStaticBlips, { LocateRecurse = true; CopyUpValues = true; } )
ReplaceUpValue( GUIMinimap.Initialize, "kBlipInfo", kBlipInfo, { LocateRecurse = true } )

The CopyUpValues allows you to replace a function without copy pasting every local variable and function it references. Just run
PrintUpValues( GetUpValue( GUIMinimap.Update, "UpdateStaticBlips" ) )
to get the list of the local variables you will need, declare them above your new function, and use the CopyUpValues option to automatically copy them over while replacing. If you want to do this while replacing class methods, you can do the same but use CopyUpValues function directly.


There are a lot more examples of the use of this in the NS2Plus code. (You will notice the utility function is in a CHUD subfolder so as to not conflict with CompMod.)
https://github.com/Mendasp/NS2Plus


Feel free to ask in this thread about specific examples of how to hook particular methods/functions/local functions, or how to deal with mod compatibility issues. Also please post in this thread if you include the file in your mod to help sort out any potential (but unlikely) conflicts! I will post in this thread when I update it with a new version, but because of how it saves old versions in memory if you have the utility file set up correctly, it won't require any action on your part. :)


And again, here's the repo for the utility library itself
https://github.com/sclark39/NS2Elixer

Comments

  • GhoulofGSG9GhoulofGSG9 Join Date: 2013-03-31 Member: 184566Members, Super Administrators, Forum Admins, Forum Moderators, NS2 Developer, NS2 Playtester, Squad Five Blue, Squad Five Silver, Reinforced - Supporter, WC 2013 - Supporter, Pistachionauts
    Great to have another lib to play around with ;)
  • McGlaspieMcGlaspie www.team156.com Join Date: 2010-07-26 Member: 73044Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester, Squad Five Blue, Squad Five Silver, Squad Five Gold, Reinforced - Onos, WC 2013 - Gold, Subnautica Playtester
    Have you done any profiling on Elixer? I'm curious what it's overhead cost is.
    Excellent work regardless ;)
  • remiremi remedy [blu.knight] Join Date: 2003-11-18 Member: 23112Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester
    Nope.

    But, most of these functions are meant to only be used on load of the mod and so they would only run once. Speed is not paramount at that point. The only one that gets run more than once really is EPrint, but that's just a Shared.Message and string.format wrapper function.
    The only other overhead would be the memory cost, but that's not much of a concern for a desktop game and a bunch of lua functions shouldn't break the bank.
    The file itself is only 6.5kb, so it also shouldn't impact the size of your mod much at all.

    So all in all I'd rate the overhead cost as nil. :)
  • remiremi remedy [blu.knight] Join Date: 2003-11-18 Member: 23112Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester
    This thread is feeling quite blue.
  • McGlaspieMcGlaspie www.team156.com Join Date: 2010-07-26 Member: 73044Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester, Squad Five Blue, Squad Five Silver, Squad Five Gold, Reinforced - Onos, WC 2013 - Gold, Subnautica Playtester
    Blue is the new Yellow ;)
  • NordicNordic Long term camping in Kodiak Join Date: 2012-05-13 Member: 151995Members, NS2 Playtester, NS2 Map Tester, Reinforced - Supporter, Reinforced - Silver, Reinforced - Shadow
    Saying cool mod, to break up the blue. It is a cool mod though.
  • kmgkmg Join Date: 2008-02-28 Member: 63758Members
  • remiremi remedy [blu.knight] Join Date: 2003-11-18 Member: 23112Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester
    edited August 2014
    - 2014-08-31: (Version 1.8)
    - Added locals(stacklevel) iterator
    - Added GetLocalsFromCallingFunction() which returns a table with name/value pairs of the calling functions
    - Added SetLocalFromCallingFunction( localname, newval ) to overwrite the value of a local in a calling function

    Example of usage from NS2+ to grab values needed from UpdateStaticBlips from within the call it makes to GUIItemSetColor:
    
    local OldGUIItemSetColor = GUIItem.SetColor
    local function NewGUIItemSetColor( blip, blipColor )
    	local vars = GetLocalsFromCallingFunction()	
    	local blipType, isHallucination, playerTeam, blipTeam, spectating, underAttack = 
    		vars.blipType, vars.isHallucination, vars.playerTeam, vars.blipTeam, vars.spectating, vars.underAttack
    	
    	if blipType and playerTeam and blipTeam then
    		if CHUDGetOption("playercolor_m") > 0 and marinePlayers[blipType] then
    			blipColor = ColorIntToColor(CHUDGetOptionAssocVal("playercolor_m"))
    		end
    		
    		if CHUDGetOption("playercolor_a") > 0 and alienPlayers[blipType] then
    			blipColor = ColorIntToColor(CHUDGetOptionAssocVal("playercolor_a"))
    		end
    
    -- ...
    	end
    	
    	OldGUIItemSetColor( blip, blipColor )
    end
    
    
    local originalMinimapUpdate
    originalMinimapUpdate = Class_ReplaceMethod( "GUIMinimap", "Update",
    function(self, deltaTime)
    	GUIItem.SetColor = NewGUIItemSetColor
    	
    	originalMinimapUpdate(self, deltaTime)
    	
    	GUIItem.SetColor = OldGUIItemSetColor
    	
    -- ...
    
  • MrFangsMrFangs Join Date: 2013-03-27 Member: 184474Members, Reinforced - Shadow
    remi.D wrote: »
    Example of usage from NS2+ to grab values needed from UpdateStaticBlips from within the call it makes to GUIItemSetColor:
    
    [...]
    
    local originalMinimapUpdate
    originalMinimapUpdate = Class_ReplaceMethod( "GUIMinimap", "Update",
    function(self, deltaTime)
    	GUIItem.SetColor = NewGUIItemSetColor
    	
    	originalMinimapUpdate(self, deltaTime)
    	
    	GUIItem.SetColor = OldGUIItemSetColor
    	
    -- ...
    

    A heads-up regarding this example: When you run this from your initial client class, it may fail as it cannot find the GUIMinimap class. When I converted my mod from class overriding to hooking, this had me confused for a while - I assumed it was some issue with missing Script.Load() imports I needed to define, but I could not get it to work, no matter what imports I tried.

    Then, I noticed that the code actually worked fine when my mod was re-run by hotloading, ie when the map and GUI were already initialized. So the GUIMinimap class simply isn't available at the time when the mod is initialized first. The way I got this to work is hooking into an event that is triggered after all initialization is done - I use Player.OnInitialized(), and it seems to work great this way:
    local originalMinimapUpdate
    
    local originalPlayerOnInitialized
    originalPlayerOnInitialized = Class_ReplaceMethod("Player", "OnInitialized",
    function(self)
    	originalPlayerOnInitialized(self)
    	if (GUIMinimap) then -- class should be available now
    		if (originalMinimapUpdate == nil) then
    			originalMinimapUpdate = Class_ReplaceMethod( "GUIMinimap", "Update",
    			function(self, deltaTime)
    				originalMinimapUpdate(self, deltaTime)
    				-- do your mod stuff here
    			end)
    			-- Print("Injected into GUIMinimap class") -- debug
    		else
    			-- Print("Skipped double method injection") -- debug
    		end
    	else
    		Print("Failed to inject into GUIMinimap class")
    	end
    end)
    


    Note that the "if(<class name>)" check is just a safeguard, and may be redundant.

    The "originalMinimapUpdate" field is defined outside of any functions, as it serves to prevent duplicate method overwriting/hooking. There may be more elegant solutions, but this seems to do the job.
  • GhoulofGSG9GhoulofGSG9 Join Date: 2013-03-31 Member: 184566Members, Super Administrators, Forum Admins, Forum Moderators, NS2 Developer, NS2 Playtester, Squad Five Blue, Squad Five Silver, Reinforced - Supporter, WC 2013 - Supporter, Pistachionauts
    edited September 2014
    Oh welcome to gui hooking :D.

    The trick i use is to hook into OnThink and then wait until the needed gui class exist in the global table then i create my hook and remove the OnThink hook.

    What ns2+ does is creating the gui hooks after the "LoadComplete" event has been called by the client.

    Both are valid tactics to go with. In the future upcoming cdt patches might hand you even more powerfull solutions to do gui hooking ;)
  • remiremi remedy [blu.knight] Join Date: 2003-11-18 Member: 23112Members, Super Administrators, Forum Admins, NS2 Developer, NS2 Playtester
    edited September 2014
    @MrFangs @Ghoul‌ofGSG9 although both will work, using the LoadComplete option will likely be more compatible with other mods. Here is the relevant bits from NS2+
    // in CHUD_Client.lua
    
    local function OnLoadComplete()
    	Elixer.UseVersion( kCHUDElixerVersion )
    	Script.Load("lua/NS2Plus/CHUD_GUIScripts.lua")
    end
    
    Event.Hook("LoadComplete", OnLoadComplete)
    

    The reason why this works well is it maintains the same load order as defined by the *.entry files (because the hook callbacks are called in the order they were registered).
Sign In or Register to comment.