• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

UnitProperty

Level 11
Joined
Nov 4, 2007
Messages
337
This is an interface between the object editor and the trigger editor. By using the tool GMSI it provides a nice functionality: It can read out object editor data from units.

The nice thing is that this is flexible and some nice ideas are behind it.

The static list
The system is able to return structs with the function GetUnitProperty(u) that contain many useful object editor data about the units, like damage or armor. But if all the data are stored into structs directly, it would be extremely bad for your memory. So this has got a nice trick:
All information the GMSI code reads out about units are stored in a big function, called the static list. If a user wants to get the UnitProperty struct of a unit the first time, a huge function is called (the static list) that runs a lot of ifs which check the unit type of the unit. At a certain point, where the unit type is 'found' the static list returns a struct using the .create method. Then the struct is stored in a table with the unit id as key.
The next time a user wants to get the information about the unit, the system reacts very quickly - because it does not have to run all the 1700 ifs. And the memory is kept low, making this system actually 'possible'.

The Flexibility
Another nice thing about this system is the flexibilty:
It would be crappy to store all the data of the units in a struct, because it would use up too much memory and runtime and most fields are probably never gonna be used, but it would be a waste to just make some fields undetectable.
So the solution I got should probably make everybody happy (Thanks for Kruemel for the idea of making this dynamic):
Users can add their own fields directly into the struct.
Each time GMSI compiles, it checks the members of the struct and generates the create method from them and fills them with the correct data. But how shall the system know the rawcode of the fields? You simply add a comment behind the member that equals to the rawcode name.
Example:
JASS:
integer attackRange // rangeN1
The accessable member will be named attackRange and equal to the rangeN1 field of the unit.

The safety
Actually this is pretty safe. The problem is that users get a lot of access to the system. Idiots can break it easily by doing things like adding commands to the fields where no commands should be (the member field for example), formatting their own members incorrectly or storing them by the wrong type (store a string in a real variable).
However, I dealt with some flaws:
  • The string \ is replaced by \\ making this actually compile
  • A field isn't just empty. There is either null or "" in it, if the Object Editor field is empty
  • Tabs can be placed in the system (" ") to have a nice formatting
  • It uses a smart way of detecting if sth. is a string in warcraft or not. So it places "" correctly.
Well, that's not much, but important.
I hope there aren't that many idiots.


The other 'features'
  • This got a very nice speed bonus because of nice string usage. Reading out big strings was reduced drastically and smartly
  • You can also select the option 'Only Custom Ids' at the GMSI Script to improve the compile time.
  • This is the only system of it's kind
  • The system is dynamic: You can use any OE field you like
  • It doesn't use up a lot of memory and it doesn't use up a lot of performance either
  • This can't be used multiple times on the same file. It will delete all the old data before adding new data.

Requirements
GMSI, Table

Basic Members
JASS:
        real dmgplus // dmgplus1
        real dice // dice1
        real sides // sides1
        real attackRate // cool1
        integer attackRange // rangeN1
        string dat // atkType1
        real armor // def
        real lifeRegeneration // regenHP
        real manaRegeneration // regenMana
        real aoe // Farea1
        string targets1 // targs1
        string targets2 // targs2
        real castPoint // castpt
        real backswing // backSw1
        real damagePoint // dmgpt1
        
        integer buildTime // bldtm
        real collisionSize // collision
        
        string model // file
        string icon // Art
        string missileModel // Missileart
                                
        integer sightRange // sight
        integer sightRangeNight // nsight
        string sourceRace // race
        real bounty // bountyplus
        string moveType // movetp
        real missileSpeed // Missilespeed

        string attribute // Primary

GMSI Code
Code:
/**
	@author Mr.Malte 
*/
	include ("string.gsl");
	include ("objects.gsl");
	include ("dialog.gsl");
	
	float CalcAverage(int basis, int dice, int sides)
	{
		return ((basis*2+sides+(sides*dice))/2);
	}
	
	// "	real Bla // HELO" ==> "real Bla" or "Bla"
	string getMemberName(string member, bool cutType)
	{
		int add = 0;
		if (strpos(member,"real ") != -1){
			if (cutType) add = 5;
			return substr(member,strpos(member,"real ")+add,strpos(member," //"));}
		else if (strpos(member,"integer ") != -1){
			if (cutType) add = 8;
			return substr(member,strpos(member,"integer ")+add,strpos(member," //"));}
		else if (strpos(member,"string ") != -1){
			if (cutType) add = 7;
			return substr(member,strpos(member,"string ")+add,strpos(member," //"));}
		return "FAIL";
	}
	
	string getMemberField(string member)
	{
		return( substr(member,strpos(member,"// ")+3,-1));
	}
	
	string cut(string which, string start, string end)
	{
		 return substr(which,0,strpos(which,start)+strlen(start))+substr(which,strpos(which,end),-1);
	}
	
	array options = array();
	bool onlyCustomIds = confirmDialog("ObjectData","Do you only need data from custom units?",BUTTONS_YES_NO);
	string Path = (@args[0] != null ? @args[0] : fileDialog("Choose your map",@inputPath,".w3x"));
	
    Map map = loadMap(Path);
	string s;
	int i = 0;
	string code = map.script.headerCode;
	string tempString;
	Trigger ObjectData;
		
	echo("\nFinding ObjectData library...\n");
	for(i : map.script.triggers){
		if (map.script.triggers[i].name == "ObjectData")
			ObjectData = map.script.triggers[i];
			break;
	}
	
	echo("Flushing old Data...\n");
	ObjectData.code = cut(ObjectData.code,"// CREATE","// ENDCREATE");
	ObjectData.code = cut(ObjectData.code,"// SYNC","// ENDSYNC");
	
	if (strpos(code,"library UnitStaticList") != -1){
		if (strpos(code,"endlibrary // X47K589687UHGFV8") == -1)
			fail("\n ObjectData library has got wrong signature.\n");
		code = substr(code,0,strpos(code,"library UnitStaticList")) + substr(code,strpos(code,"endlibrary // X47K589687UHGFV8")+strlen("endlibrary // X47K589687UHGFV8"),-1);
		map.script.headerCode = code;
		}
	
	echo("Compiling desired Data...\n");
	int createIndex = strpos(ObjectData.code,"// CREATE")+9;
	string createMethodCode = "		static method create takes integer id, ";
	string syncCode = "";
	array members = splitstr(substr(ObjectData.code,strpos(ObjectData.code,"// MEMBERS")+10,strpos(ObjectData.code,"// ENDMEMBERS")),"\n");
	int fieldCounter = 0;
	array fields = array();
	string member;
	for(i : members){
		// Getting the members unformatted.
		members[i] = replaceAll(substr(members[i],0,strlen(members[i])-1),"    ","");
		if (strlen(members[i]) > 5)
		{
			fieldCounter = fieldCounter + 1;
			fields[fieldCounter] = getMemberField(members[i]);
			member = getMemberName(members[i],true);
			if (member != "FAIL")
				{
				syncCode = syncCode + "			set up."+member+" = "+member+"\n";
				member = getMemberName(members[i],false);
					createMethodCode = createMethodCode + member + ", ";
				}
		}
	}
	createMethodCode = substr(createMethodCode,0,strlen(createMethodCode)-2) + " returns UnitProperty";
	ObjectData.code = substr(ObjectData.code,0,createIndex) + "\n" + createMethodCode+"\n" + substr(ObjectData.code,createIndex,-1);
	int syncIndex = strpos(ObjectData.code,"// SYNC")+7;
	ObjectData.code = substr(ObjectData.code,0,syncIndex) + "\n" + syncCode+"\n" + substr(ObjectData.code,syncIndex,-1);
	i = 0;
	
	int fieldIndex;
	
	s = "";
	code = "";
	echo("\nInstalling Static List...\n\n");
	code = code + "\nlibrary UnitStaticList";
	code = code + "\nfunction GetUnitPropertyStatic takes integer id returns UnitProperty";
	code = code + "\n" + "local UnitProperty up";
	code = code + "\n" + "if id == 0 then";
	code = code + "\n" + "return 0";
	
    for(s: map.objects){
        Wc3obj obj = map.objects[s];
        if(!(obj instanceof Unit)) continue;
		if(obj.race == "") continue; // There is exactly one unit that has no race.
		if(isIdPredefined(s) == true && onlyCustomIds == true) continue;
			
		code = code + "\n elseif id == '"+s+"' then \n return UnitProperty.create(id";
		fieldIndex = 0;
		for(fieldIndex:fields)
			{
			
			if ((string)obj[fields[fieldIndex]] == "")
				code = code + ", null";
			else if (toUpperCase(obj[fields[fieldIndex]]) != (string)obj[fields[fieldIndex]]
				|| toLowerCase(obj[fields[fieldIndex]]) != (string)obj[fields[fieldIndex]])
					code = code + ", \"" + replaceAll(obj[fields[fieldIndex]],"\\\\","\\\\\\\\") + "\"";
				else
					code = code + ", "+obj[fields[fieldIndex]];
			}
		code = code + ")";
		
		
		// Warcraft can't handle too many ifs. It was hard to find the bug, but I could fix it.
		i = i + 1;
		if (i%50 == 0)
		{
			code = code + "\nendif";
			code = code + "\n\nif 0 == 1 then\nreturn 0";
			echo("=");
			map.script.headerCode = map.script.headerCode + code;
			code = "";
		}
		

    }
	echo("\nFinishing library...");
	code = code + "\n" + "endif";
	code = code + "\n" + "return -1";
	code = code + "\n" + "endfunction";
	code = code + "\n" + "endlibrary";
	echo("\nSetting library signature.");
	code = code + " // X47K589687UHGFV8";
	map.script.headerCode = map.script.headerCode + code;
	saveMap(map,"output/UDIOutput.w3x");

vJass Code
JASS:
library UnitDataInterface initializer init requires Table, UnitStaticList
//===========================================================================
// Credits:
// =======
//  I'd really like to thank gekko for his script language GSL
//  and his programm GMSI. Without that stuff this system would not have been
//  possible.
//
//  The same thing goes for vJass.
//
//  This consumed some nasty hours for finding the sticks warcraft throws between my legs.
//  Some wc3 bugs are really bad...
//  Also it was a lot of work to make it accurate, flexible and save against idiots.
//
//===========================================================================
// Information: 
//==============
//
//  This is the only jass system in existance that can return unit data from the
//  object editor like damage, sight range and attackrate.
//  This can also be used in addition with the library BonusMod.
//  It works by using gekkos script language GSL - i've written a script in that
//  language that reads all the object editor data out and stores them in UnitProperty
//  structs.
//
//  The UnitProperty structs are dynamic! So if you remove fields from it, because
//  you don't need them, it will not break the system. You can also
//  add new fields to be read out. Read 'Adding new fields' in order to do that.
//
//===========================================================================
// Functions:
// ==========
//
//  GetUnitProperty(unit)   : Returns a struct that contains all important OE data about the unit.
//  UnitProperty[unit]      : The same thing with a different syntax.
//
//===========================================================================
// Implementation:
// ===============
//
//  1. Make sure you have the latest JassHelper or JassNewGen:
//   [url]http://www.wc3c.net/showthread.php?t=90999[/url]
// 
//  2. Install GMSI like described here:
//     [url]http://www.wc3c.net/showthread.php?t=106485[/url]
//
//  3. Make sure you installed the vJass system 'Table':
//   [url]http://www.wc3c.net/showthread.php?t=101246[/url]
//
//  4. Convert a new trigger to custom text and insert this whole code.
//      Then save the map.
//
//  5. Download the UnitDataInterfaceGMSI file from this link: 
//      ===
//
//  6. Start GMSI and choose the UnitDataInterface GMSI file from where you copied it.
//      Press two times on it and select your map with a double click.
//
//  7. The map will be saved in the folder output with the name 'UDIOutput.w3x'
//   
//===========================================================================
// Adding new fields:
// =================
//
//  If you want to read out additional data, you can do these steps to do it:
//
//  1. Add the member name to the struct.
//
//  2. Go into the object editor
//
//  3. Select the field you want to read out
//
//  4. Press 'Strg + D'
//
//  5. Now the names of all fields have changed.
//      Write '// ' + the name of the field behind the member name.
//
//  Example: startMana // mana0  
//===========================================================================
// GMSI Script:
// ===========
//
// Can be found here:
// [url]http://peeeq.de/code.php?id=2881[/url]
//
// It is neccesary!
//
//===========================================================================
// FAQ:
// ===
//
// 1. If the systems stores so many object editor data in a struct for each unit type.
//    Wont that use huge amounts of memory?
//      Answer: No it wont. The GMSI script generates a function that contains a list with all unit
//              types. This list is static. I need many ifs to find out the correct unit and create
//              a correct UnitProperty struct, but if you used a UnitProperty struct for a unit once,
//              it is saved in a Table.
//
//===========================================================================

    globals
        private Table Data
    endglobals

    private function GetUnitPropertyPrivate takes integer id returns UnitProperty
        local UnitProperty up = Data[id]
        if up != 0 then
            return up
        else
            set up = GetUnitPropertyStatic(id)
            if up == 0 then
                call BJDebugMsg("UnitDataInterface: Choosed invalid id.")
            endif
            set Data[id] = up
            return up
        endif
    endfunction
    
    function GetUnitProperty takes unit who returns UnitProperty
        return GetUnitPropertyPrivate(GetUnitTypeId(who))
    endfunction
    
    function StringContains takes string u, string targetType returns boolean
        local integer i = 0
        local integer length = StringLength(targetType)

        loop
            exitwhen i > StringLength(u)
            if SubString(u,i,i+length) == targetType then
                return true
            endif
            set i = i + 1
        endloop
        return false
    endfunction
    
    function UnitAttacksType takes unit u, string targetType returns boolean
        local UnitProperty up = GetUnitProperty(u)
        return StringContains(up.targets1+up.targets2,targetType)
    endfunction
    
    // I hate blizzard for some reasons..
    function ConvertAttribute takes string s returns integer
        if s == "INT" then
            return bj_HEROSTAT_INT
        elseif s == "AGI" then
            return bj_HEROSTAT_AGI
        elseif s == "STR" then
            return bj_HEROSTAT_STR
        endif
        debug call BJDebugMsg("UnitDataInterface: Invalid string in 'ConvertAttribute'")
        return 0
    endfunction
    
    // Returns the average damage of a unit.
    // If a user has got bonusMod in his map it has to be changed.
    function GetUnitDamage takes unit u returns real
        local UnitProperty up = GetUnitProperty(u)
        if IsUnitType(u,UNIT_TYPE_HERO) then
            return 0.5*(up.minDamage + up.maxDamage)+GetHeroStatBJ(ConvertAttribute(up.attribute),u,true)
        else
            return 0.5*(up.minDamage + up.maxDamage)
        endif
    endfunction
        
    function GetUnitAttacktype takes unit u returns attacktype
        local UnitProperty up = GetUnitProperty(u)
        if up.dat == "normal" then
            return ATTACK_TYPE_NORMAL
        elseif up.dat == "pierce" then
            return ATTACK_TYPE_PIERCE
        elseif up.dat == "hero" then
            return ATTACK_TYPE_HERO
        elseif up.dat == "siege" then
            return ATTACK_TYPE_SIEGE
        elseif up.dat == "magic" then
            return ATTACK_TYPE_MAGIC
        elseif up.dat == "spells" then
            return ATTACK_TYPE_MAGIC
        elseif up.dat == "chaos" then
            return ATTACK_TYPE_CHAOS
        elseif up.dat == "unknown" then
            return null
        endif
        debug call BJDebugMsg("UnitDataInterface: Can't get the attacktype of "+GetUnitName(u)+".")
        return null
    endfunction
                                
    struct UnitProperty
        static method operator[] takes unit key returns UnitProperty
            return GetUnitPropertyPrivate(GetUnitTypeId(key))
        endmethod
        
        // These members shall not be registered by the system: They're special.
        // We place them outside of the system.
        real minDamage
        real maxDamage
        
    // DO NOT PLACE ANYTHING ELSE (LIKE COMMENTS) BUT MEMBERS IN THE MEMBERS SECTION!
    // MEMBERS
        integer dmgplus // dmgplus1
        integer dice // dice1
        integer sides // sides1
        real attackRate // cool1
        integer attackRange // rangeN1
        string dat // atkType1
        integer armor // def
        real lifeRegeneration // regenHP
        real manaRegeneration // regenMana
        real aoe // Farea1
        string targets1 // targs1
        string targets2 // targs2
        real castPoint // castpt
        real backswing // backSw1
        real damagePoint // dmgpt1
        
        integer buildTime // bldtm
        real collisionSize // collision
        
        string model // file
        string icon // Art
        string missileModel // Missileart
                                
        integer sightRange // sight
        integer sightRangeNight // nsight
        string sourceRace // race
        integer bounty // bountyplus
        string moveType // movetp
        real missileSpeed // Missilespeed

        string attribute // Primary
    // ENDMEMBERS
        
        // CREATE
        static method create takes integer id returns UnitProperty
        // ENDCREATE
            local UnitProperty up = UnitProperty.allocate()
            // SYNC
            // ENDSYNC
            set up.minDamage = up.dmgplus+up.dice
            set up.maxDamage = up.dmgplus+(up.dice*up.sides)
            set up.dice = 0
            set up.sides = 0
            set up.dmgplus = 0
            set up.model = up.model + ".mdl"
            set Data[id] = up
            return up
        endmethod
    endstruct
    
    private function init takes nothing returns nothing
        set Data = Table.create()
    endfunction
endlibrary

Why the name 'UnitDataInterface'?
Well, UnitProperty was already reserved.
ObjectEditorData wouldn't fit so much, because users could think it's for all kinds of objects (like abilities)
Actually this is the interface between jass and the object editor.


Theoretical usages

  • You can make a trigger that stops ranged attacks and imitates them. The missiles are triggered and you can access all missiles then.
 

Attachments

  • UnitPropertyOutput.w3x
    72.7 KB · Views: 82
Last edited:
Level 3
Joined
Mar 7, 2004
Messages
37
Edit: Forgot I replied to this. There are better ways to do it so never mind.
 
Last edited:
Level 8
Joined
Oct 3, 2008
Messages
367
I find that the way this works is far more elegant than the Object Data Extractor. Intuitive, and easy to use. I'm going to approve this after you make these changes:

When you have time, feed your documentation through a spell checker or something. There are some minor errors.

Also, a different name would be welcome. Very similar to UnitProperties, and could cause confusion.
 
Last edited:
Level 5
Joined
Dec 1, 2008
Messages
120
Your .gsl script is flawed. Did you ever test it yourself?

Code:
	echo("\nFinding ObjectData library...\n");
	for(i : map.script.triggers){
		if (map.script.triggers[i].name == "ObjectData")
			ObjectData = map.script.triggers[i];
			break;
	}
This works only if a trigger object with name ObjectData is the very first in the trigger editor (if it should be like that, no loop would be required). You can probably see that the loop will break in the first iteration no matter what. ({} *wink wink)

Secondly, the GetUnitPropertyStatic in my map...

JASS:
function GetUnitPropertyStatic takes integer id returns UnitProperty
local UnitProperty up
if id == 0 then
return 0
 elseif id == 'proj' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
 elseif id == 'h000' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
 elseif id == 'h001' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
 elseif id == 'h002' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
 elseif id == 'h003' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
 elseif id == 'h004' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
 elseif id == 'h005' then 
 return UnitProperty.create(id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)
endif
return -1
endfunction

---------------------------------------

I'm not quite sure if this submissions name is UnitProperty, UnitDataInterface, or ObjectData? You never mentioned that the trigger object must be named ObjectData in order to make this thing work.

I cba to find out what went wrong with writing the UnitStaticList. THIS NEEDS MASSIVE REWORK.

EDIT//: The problem below might've been mistake on my side. I didn't sync GMSI properly..
 
Last edited:
Top