How to integrate options into the Cosmos Menu

By Alex Brazie

With the increasing number of features available in Cosmos, the need for a consistent way to encapsulate menu options has become nessecary. Instead of having to sort through a million documents to find the features we want, a menu screen containing all of the new features should be available. This will not only make it possible to activate and change settings easily, it will also advertise lesser-known features such as QuestPublishing and modified hotkeys.

So with this goal in mind, I created a system designed to be:

Simple:

Registration consists of a single command.

Consistent:

The same parameters are used for different types of input, and all input is updated the same way.

Flexible:

There are four different styles of menu option, and callbacks can be any lua function with 1 argument.

Robust:

There's as much error checking code in the registration command as possible, so that if you leave out a parameter from the end of the registration command, it should still work.


Okay, thats great, but how do I use it?

Alright, lets run though a short tutorial that uses CosmosMenu (ESM). Here is an example “Normal” Blizzard UI XML file:

CombatCaller.xml
<Ui xmlns=http://www.blizzard.com/wow/ui/
xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xsi:schemaLocation="http://www.blizzard.com/wow/ui/
C:\Projects\WoW\Bin\Interface\FrameXML\UI.xsd">
<Script file="CombatCaller.lua"/> <Frame name="CombatCaller" parent="UIParent"> <Size> <AbsDimension x="192" y="32"/> </Size> <Anchors> <Anchor point="BOTTOM"> <Offset> <AbsDimension x="0" y="64"/> </Offset> </Anchor> </Anchors> <Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true"> <BackgroundInsets> <AbsInset left="4" right="4" top="4" bottom="4"/> </BackgroundInsets> <TileSize> <AbsValue val="32"/> </TileSize> <EdgeSize> <AbsValue val="13"/> </EdgeSize> </Backdrop> <Layers> <Layer level="ARTWORK"> <FontString inherits="SystemFont" text="Autocamp 2000"> <Anchors> <Anchor point="LEFT"> <Offset> <AbsDimension x="48" y="0"/> </Offset> </Anchor> </Anchors> </FontString> </Layer> </Layers> <Scripts> <OnLoad> CombatCaller_OnLoad(); </OnLoad> <OnEvent> CombatCaller_OnEvent(event); </OnEvent> <OnUpdate> CombatCaller_OnUpdate(arg1); </OnUpdate> </Scripts> <Frames> <CheckButton name="CombatCallerEnable"> <Size> <AbsDimension x="32" y="32"/> </Size> <Anchors> <Anchor point="LEFT">s <Offset> <AbsDimension x="8" y="0"/> </Offset> </Anchor> </Anchors> <Scripts> <OnClick> CombatCaller_Toggle(); </OnClick> </Scripts> <NormalTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/> <HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/> <CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/> </CheckButton> </Frames> </Frame> </Ui>

Now, look at all the stuff this XML file has to manage! Backgrounds, Event handlers, Textures, dimensions, alpha modes, and more! Sure, this is powerful, and effective, but what did the author of this XML file really want? A checkbox to turn his mod on and off.

All of that work for a couple event handlers and a checkbox. OUCH! Even worse, this mod will always be displayed in the UI. You can't make it disappear, and you can't really do much without a lot of code to change it. Now, its arguable that that's what he wanted, and if so, great. Yet, if you have 20 different options, each with its own window, things can get cluttered and ruin the user's viewscreen. Instead, lets take this guys code and move it into ESM's event tracking and registration system.

First thing, we have to figure out whether this is a module that needs its own frame system.  For example, KillLog, QuestShare, and company all create their own custom frames and require that structure to remain intact. If so, its XML file can simply be added to the end of the CosmosIncludes.xml file provided with Cosmos. However, if it doesnt need its own pane, you can simply add its event handlers to the generic frame given in the CosmosIncludes.xml file.

CosmosIncludes.xml
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
C:\Projects\WoW\Bin\Interface\FrameXML\UI.xsd">
	<!-- 
		Include your own pure-scripts here. 
	-->
	<Script file="CosmosGuiConfig.lua"/>
	<Script file="CombatCaller.lua"/>
	<Frame name="PureScriptEventsFrame" parent="UIParent" hidden="true">
		<Scripts>
			<OnLoad>
				Cosmos_RegisterAlphas();
				CombatCaller_OnLoad();
			</OnLoad>
			<OnEvent>
				CombatCaller_OnEvent(event);
			</OnEvent>
			<OnUpdate>
				CombatCaller_OnUpdate(arg1);
			</OnUpdate>
		</Scripts>
	</Frame>
	<!--
		Include self-contained frames and scripts here. 
	-->	
	<Include file="Interface\FrameXML\CombatHelper.xml"/>
</Ui>

Now, for example here, CombatHelper uses its own  XML frame system. Since ChoppedLiver (Author of CombatHelper) wants to keep his code modular, he simply includes it here to ensure that CombatHelper can access the Register functions. However, in the case of CombatCaller (Written by our mystery author) there's no need for its own frame code, so he simply addsthe CombatCaller_OnEvent and CombatCaller_OnLoad() commands after the ones already in Cosmos.  The order that the load and include commands occur is very important to the final menu.  However, so long as the commands occur in the CosmosIncludes.xml file, it should be Okay.

Now, our author of CombatCaller has deferred all of his event code into the include file, but how does he actually tell CosmosMenu that a new object exists? Lets take a look at his LUA code.

CombatCaller.lua
--[[
 Combat Caller
    By Alex Brazie
  
  Automates Low-HP and Out-Of-Mana calls

  This was written for testing of the shared
  configuration module I'm writing. 
  (But first, I need a module to configure!) 
  
  ]]--

-- These will the the values with checkboxes
CombatCaller_HealthCall = true;
CombatCaller_ManaCall = true;

-- These will be the variable variables. ;)
CombatCaller_HealthRatio = .4;
CombatCaller_HealthLimit = 100;
CombatCaller_ManaRatio = .3;
CombatCaller_ManaLimit = 150;

function CombatCaller_OnLoad() 
	this:RegisterEvent("UNIT_HEALTH");
	this:RegisterEvent("UNIT_MANA");
	
	this.hp = -1;
	this.mana = -1;
	this.lasthp = -1;
	this.lastmana = -1;
	
	-- Register with the CosmosMaster
 	CosmosMaster_Register(
 		"ES_COMBATCALLER_HEALTHSLIMIT", --CVar
 		"BOTH",									 --Things to use
 		"Enable Auto Low Health Shout",			 --Simple String
 		"Automatically notifies your party\n when your health drops below a certain level.",
 												 --Description 
 		CombatCaller_OnHealthConfigUpdate,		 --Callback
 		1,										 --Default Checked/Unchecked
 		.2,										 --Default Value
 		0,										 --Min value
 		1,										 --Max value
 		"Health Limit",							 --Slider Text
 		.01,									 --Slider Increment
 		1,										 --Slider state text on/off
 		"\%"									 --Slider state text append
 		);
 	CosmosMaster_Register(
 		"ES_COMBATCALLER_MANASLIMIT", 
 		"BOTH", 
 		"Enable Auto Out Of Mana Shout", 
 		"Automatically notifies your party\n when your mana drops to the specified level.",
 												 --Description 
 		CombatCaller_OnManaConfigUpdate,
 		1, 
 		.2, 
 		0, 
 		1, 
 		"Mana Limit", 
 		.01, 
 		1, 
 		"\%"
 		);
end

function CombatCaller_OnEvent(event) 
	if ( UnitIsDead("player") ) then
		SendChatMessage("I'm dead.");
		return;
	end
	if ( event == "UNIT_HEALTH" ) then
		local ratio = UnitHealth("player")/UnitHealthMax("player");
		local oldratio = this.lasthp/UnitHealthMax("player");

		
		if ( (this.hp < this.lasthp) and (ratio < CombatCaller_HealthRatio) and (oldratio > CombatCaller_HealthRatio) ) then
			CombatCaller_ShoutLowHealth();
		end
		
		this.lasthp = this.hp;
		this.hp = UnitHealth("player");
	end
	if ( event == "UNIT_MANA" ) then
		local ratio = UnitMana("player")/UnitManaMax("player");
		local oldratio = this.lastmana/UnitManaMax("player");

		if ( (this.mana < this.lastmana) and (ratio < CombatCaller_ManaRatio) and (oldratio > CombatCaller_ManaRatio) ) then
			CombatCaller_ShoutLowMana();
		end
	
		this.lastmana = this.mana;
		this.mana = UnitMana("player");
	end
end

function CombatCaller_OnHealthConfigUpdate(value,toggle) 
	if ( toggle == 0 ) then
		CombatCaller_HealthCall = false;
	else 
		CombatCaller_HealthCall = true;
		CombatCaller_HealthRatio = value;
	end
end

function CombatCaller_OnManaConfigUpdate(value,toggle) 
	if ( toggle == 0 ) then
		CombatCaller_ManaCall = false;
	else 
		CombatCaller_ManaCall = true;
		CombatCaller_ManaRatio = value;
	end
end

function CombatCaller_ShoutLowHealth()
	if ( CombatCaller_HealthCall ) then
		PlayVocalCategory(8);
	end
end

function CombatCaller_ShoutLowMana()
	if ( CombatCaller_ManaCall ) then
		PlayVocalCategory(5);	
	end
end

function CombatCaller_TurnOff()
	CombatCaller_HealthCall = false;
	CombatCaller_ManaCall = false;
end

function CombatCaller_TurnOn()
	CombatCaller_HealthCall = true;
	CombatCaller_ManaCall = true;
end
	
					
					

Wow, thats a lot of code, huh? But lets break it down.

There's the OnLoad, OnEvent and other basic event handlers that he wrote and included in the CosmosIncludes.xml. ... oh ? Whats this?

The Register Call
s
		 -- Register with theCosmosMaster									
		 CosmosMaster_Register( "ES_COMBATCALLER_HEALTHSLIMIT", --CVar 
		 "BOTH",								--Type
 		"Enable Auto Low Health Shout",			--Simple String
 		"Automatically notifies your party\n when your health drops below a certain level.",
 								--Description 
 		CombatCaller_OnHealthConfigUpdate,		--Callback
 		1,						--Default Checked/Unchecked
 		.2,						--Default Value
 		0,						--Min value
 		1,						--Max value
 		"Health Limit",	                                --Slider Text
 		.01,						--Slider Increment
 		1,						--Slider state text on/off
 		"\%"						--Slider state text append
 		);

Aha! The key command. This is the method that registers his program with the CosmosMenu. 

The First parameter is required! Its the Cvar that your status will be stored under. If a global variable with the same name happens to exist, that global variable will ALSO be updated. However, this occurs on a timed basis, and therefore isn't the best way to get your state.  It also must be Prefixed by "ES_" or you get a registration error. This is done to prevent conflicts with other global variables.

The 2nd Parameter is the Type. This is the type of config input you want to recieve. Legal types are: CHECKBOX (Just a checkbox), SLIDER(Just a slider bar), BOTH(BothCheckbox and slider), BUTTON(just calls your callback function) and SEPARATOR (Provides a graphical split between the previous commands and the current one).

The 3rd Parameter is the Short string to describe what you're adjusting. If its a SEPARATOR object, this is the separator text.

The 4th Parameter is the Long Description. This is the text that appear in a tooltip if you hover over the option in the Menu.

The 5th Parameter is the Callback function. This is the most important part of ESM. This function will be called every time the GUI object associated with this registration is changed. Plus once upon registration to set the default value from the CVar.  Every time a slider, checkbox is changed, this function gets called. If you're using a "BUTTON" type, then this is the function that gets called when the user clicks the button.

The 6th Parameter is the Checked/Unchecked value for checkbox types. 1 is checked. 0 is not.  (If you're not a checkbox, leave this as 1).

The 7th Parameter is the default value. If you're using "SLIDER" or "BOTH", this value is the value that will be used if the CVar doesnt exist.

The 8th Parameter is the Slider Minimum value. If the slider is being pushed all the way to the left. This value will be returned.

The 9th Parameter is the Slider Maximum value. If the slider is all the way to the right, you get this value.

The 10th Parameter is the Slider Text. Leave this blank if you dont want to describe what the slider adjusts.

The 11th Parameter is the Slider increment. If you want to control how many intermediate values are returned by the slider. Change this. It must be a value less or equal to the difference between the max and min value.

The 12th Parameter is an on/off toggle for Slider state. Turn it to 0 if you dont want the user to see the exact value returned by a slider.

The 13th Parameter is an append text for the slider state text. For example, a min and max of 0  and 1 with an append of "\%" will return a Percentage from 0 to 99%.

Thats it!

If you don't need all of these parameters, set them to null, or just dont specify them.

CosmosRegister("ES_MYCVAR", "BUTTON", "QuickButton",nil, function (x) SendChatMessage(x); end );

Is a valid Registration. (Or at least it should be!)

In the future specifying the same name for a Cvar that is already registered will cause you to have an error. Be warned!

Happy configuring.

Alex


Registering a /ChatCommand

This is easy, I hope!

	-------------------------------------------------------------------[[

	How to register a chat command:
	
	]]--
	
	--Create a function for your command:
	function myfunc () return 4; end
	
	--Create a list of /commands you want applied. 
	mycommands = { "/mycommand", "/mycommands" };
	
	--Create a help description 
	local mydesc = "This is my command!";
	
	--Pick a name or an action you are overwriting.
	local myfuncname = "CUSTOMMINE";
	
	--Choose how to handle a command if you overwrite it. (usually ESM_CHAINNONE)
	local mychain = ESM_CHAINNONE; 
	
	--Register it
	CosmosMaster_RegisterChatCommand( myfuncname, mycommands, myfunc, mydesc, mychain );
	
	-- Thats it!