The "Argh is an idiot about LUA" thread.

The "Argh is an idiot about LUA" thread.

Discuss Lua based Spring scripts (LuaUI widgets, mission scripts, gaia scripts, mod-rules scripts, scripted keybindings, etc...)

Moderator: Moderators

Post Reply
User avatar
Argh
Posts: 10920
Joined: 21 Feb 2005, 03:38

The "Argh is an idiot about LUA" thread.

Post by Argh »

Ok, here's my thread, where I will happily confess my ignorance and hopefully cure it. I have sat down with the LUA references, and am now hacking away. Hopefully, this thread will be useful to somebody, but at the very least, you folks who already know this stuff can chuckle at my lack of expertise ;)

Now, this series of posts, or bloglike chatter, is intended mainly for people, like myself, who may have some experience with BOS or other C-like languages, but find LUA baffling at first.

If you've never, ever, programmed anything before, I strongly urge you to check out the official LUA references, which I am also making use of very heavily here, as well as liberally stealing from (working) Spring LUA stuff. I'm not going to cover a lot of topics that you should know about, and I'm going to skip a lot, so you won't see it here. Just bear that in mind!

***********************************************************
***********************************************************
***********************************************************

LUA presents some challenges for me... the primary one being how to work with Functions and Tables- two of the main structural concepts behind LUA.

LUA is extremely flexible about the structure of Tables, which can be used as arrays. Most languages that I know of want arrays to be "typed" fairly strictly- LUA apparently doesn't care. This is unfamiliar ground.

For people who've never worked with Tables ever before, a Table is, basically, a list of items, stored in an exact order:

Code: Select all

t = { 1,1,2,3,5,8,13 }
They can be multiple types- you can even store the resulting values from Functions within a Table.

"Why, gosh, that's great, Argh... but why do we care about this?"

Well, the main reason to care, in the context of LUA, is that you can use Tables within and in between Functions, to move variable values around in an easy way, or to grab a lot of data from multiple sources and then present it to the user. Think of it as a way to organize your data for later use, basically. Most LUA Functions written thus far for Spring include a lot of Tables.

Now, let's start putting that Table to some sort of practical use.

Let's say that we've created our Table, t. Now we want to get the value of the third item.

Ok, so I can write code like this:

Code: Select all

print( t[3] )
Which would result in the value of the third item in the Table, t, being printed.

In fact, just to get this to sink in, here is "hello world" in LUA, using a Table:

Code: Select all

t = {"goodbye cruel world","I prefer ham sandwiches","hello world"}
print t[3]
... results in "hello world". Get it?


However! You can't just do that, in Spring! Nope, you've gotta specify what method you're going to use to print things. Here's a "hello world" example, using the Spring Messagebox:

Code: Select all

Spring.Echo('hello world')
Obviously, we don't want to use that, for all situations- i.e., most of the time, we want this information described in a Tooltip. Therefore, we want to use stuff like:

Code: Select all

MyNewbieCmdDesc.tooltip = ("hello world")
Coupled with a formal declaration of a "button" within the LUA UI:

Code: Select all

local MyNewbieCmdDesc = {
  id     = CMD_MYNEWBIE,
  type   = CMDTYPE.ICON,
  name   = 'My Newbie Code Project',
  cursor = 'Newbie Code!',  -- add with LuaUI?
  action = 'MyNewbie',
}
As you can see, we need to use Spring "hooks" to communicate with Spring, in the ways that we want to. Now... the hard part (at least, for me): writing Functions, so that we can actually make stuff happen!
Last edited by Argh on 27 Aug 2007, 09:40, edited 2 times in total.
User avatar
Argh
Posts: 10920
Joined: 21 Feb 2005, 03:38

Post by Argh »

Functions! They're where we make things actually happen. This is where all the action is.

This is actually a very familiar concept- BOS is just a series of Functions, some of which are invoked through the COBHandler in Spring, such as Killed() and Create(), and some of which are called, or started, in BOS code, via start-script and call-script.

Functions, in BOS, either operate on a millisecond timer, or they run if a condition == SomeNumber, generally speaking (there are exceptions, but they're not very commonly used).

In LUA for Spring... it doesn't work like that. While it's certainly possible to create Functions that work on a timed scale, and there are extant examples of this available, most LUA scripts work based on events, which are either generated from user input to the UI or from other sources (primarily LuaCob).

So, in order to do anything really cool with Spring, you need to write Functions.

Basic syntax time. A Function has to have the following syntax:

Code: Select all

function MyFunction(MyArgument1, MyArgument2)
end
So, we have a start and an end, just like in BOS, or C/++ for that matter, where we have Functions that look like this:

Code: Select all

MyFunction(MyArgument1, MyArgument2)
{ 
return (0);
}
The key difference here, is that the arguments in LUA do not have to be local variables. In BOS, we have to move global variables (static-vars) into local functions. As I've talked about previously, it's more efficient to move a static-var into a local variable, if you're going to use it a lot during a script, than to move it every time you need it, due to the way that low-level COB instructions work. In LUA, you don't necessarily have to do this, but most of the code I'm seeing is very explicit about local definitions, so I suspect it's a big deal.



So, let's write a simple set of Functions, that assigns a button (that, for now, does nothing) to an explicit Unit. Here I'm drawing very heavily upon the Barrage Spell code- there are other ways to do this, but this is very straightforward.

1. Let's get all of the Unit names from Spring, so that we can find the right name to work with:

Code: Select all

unitDefByName = {}
for k,ud in pairs(UnitDefs) do
        unitDefByName[ud.name] = ud
end
What does this do? Well, at first, I found this utterly confusing... "WTF is 'ud', anyhow? What are these letters being used? Haven't these people heard of descriptive variable names?"

Now that I'm starting to develop a clue... let's walk through this:

unitDefByName = {} Means that this is a Function that is empty of arguments.

for k,ud in pairs(UnitDefs) do This is a very confusing bit, and I had to look at the LUA references for awhile to figure it out.

"for / do" is equivalent to "for / next". So we're looking at a for / next loop here.

"k,ud", says that we're looking for "keys", and "ud" is a variable declaration, which you can do just about any time you want to, in LUA.

"in pairs" specifies how we want this data returned. It says, "put these together, into one resulting value", I think.

(UnitDefs) is, I think, a Spring-specific call, asking for Spring to search the textfile FBI stuff. I'm still not sure what it's doing.

So, basically, this one line is doing something pretty complicated- it's saying, "go get every UnitDef's text, put it into a paired list with 'ud' and then return".

unitDefByName[ud.name] = ud This line is where we're finally taking the results of this for / do loop, and we're storing them in a single variable. I don't quite get the "= ud" at the end, that part of the syntax bothers me, now that I've read the Tables description, but I'm sure whoever wrote that knew what they were doing a lot better than I ever will. The .name thing also bothers me- it looks like a Spring hook, getting the Name of the Unit. Not quite sure yet, frankly.

end Ends the Function.

So, we've got this unitDefByName, that passes the Names of all Units to the script. Needless to say, this has gotta be a rather lengthy operation, and you probably never, ever want to do this during the game, for example. And if you have multiple Functions that need to know Unit Names in order to work right... they should probably all use this one Function, instead of unique ones. In fact, I'd say that this sort of Function should probably become a standardized library at some point, for really common things, so that coders aren't reinventing the wheel, and wasting people's times waiting for a game to load... but I digress.

Next up... taking that information, and sorting it out!
User avatar
AF
AI Developer
Posts: 20687
Joined: 14 Sep 2004, 11:32

Post by AF »

actually

Code: Select all

unitDefByName = {} -- 1
for k,ud in pairs(UnitDefs) do -- 2
        unitDefByName[ud.name] = ud -- 3
end -- 4
1 Create a table called unitDefByName that is empty (hence empty brackets{})
2 for every key (k) value(v) pair in the UnitDefs table do the following. UnitDefs is a global table exposed by lua. Its keys are the unitIDs and the associated values are tables containing UnitDefinition data in tables
3 Put the unitdef in the table using its name as the key
4 end the loop

for example

Code: Select all

a = {}
a["key1"]="value1"
a["key2"]="value2"
a["key3"]="value3"

for k,ud in pairs(a) do
        Spring.Echo("key: ".k." value:".v)
end
That code would print:

Code: Select all

key: key1 value: value1
key: key2 value: value2
key: key3 value: value3
btw . appends strings together iirc
Andrej
Posts: 176
Joined: 13 Aug 2006, 18:55

Post by Andrej »

while we are at failing at lua coding try this:

a = {}
a[1] = 3
b = a
b[1] = 5

print("A is: " .. a[1])
print("B is: " .. b[1])
User avatar
Argh
Posts: 10920
Joined: 21 Feb 2005, 03:38

Post by Argh »

Ok, so we've got all of the Unit Names. Great. Now what? Kind've useless, if we cannot tie it either to a Unit's COB script, or to a UI command.

So, let's do the first part, making an Icon:

Code: Select all

function AddNewbieCommand(unitID, unitDefID, teamID)
        cmd = {
                id=Cmds.NEWBIE_COMMAND,
                type=CMDTYPE.ICON_MAP,
                name="Newbie Command!",
                action="newbie_command",
                tooltip="Causes stuff to happen, yay!",
                params={"0", "None"}
        }

        -- put command at the end of command list
        Spring.InsertUnitCmdDesc(unitID, 666, cmd)
end
What we have, above, is a Function that passes a lot of very specific information to Spring's pre-existing LUA UI code, in a format it recognizes.

You do not have to do this. In fact, you could just make whole new elements, and ignore all of the LUA UI code, except for the hooks into Spring, to provide data access and drawing / text / sound. However, we're newbies, so we're going to use Spring's UI, which is better than anything we're likely to code for a very long while :-)

Now, for the second part, assigning this button to a specific Unit, our NewbieCommander:

Code: Select all

function UnitCreated(unitID, unitDefID, teamID, builderID)
        if unitDefID == unitDefByName["NewbieCommander"].id then
                AddNewbieCommand(unitID)
        end
end
Ok, so any Units named "NewbieCommander" will now show this button. I'm still pretty confused about why there are so many arguments on this Function, when it only requests unitDefID and unitID, but I'm sure there was a reason.

Now... this is interesting stuff... apparently, if we don't tell Spring not to show the UI buttons for Units we don't control, it will do so by default. So, we need to add the following, to make sure we don't cause problems:

Code: Select all

function AllowCommand(unitID, unitDefID, teamID, cmdID, params, options)
        -- allow anything if it's not ours
        if not OurCmd[cmdID] then
                return true
        end
        -- if no hook defined, allow
        if AllowHooks[cmdID] == nil then
                return true
        end
        return AllowHooks[cmdID](unitID, unitDefID, teamID, cmdID, params, options)
end
The code above does a couple of other, important things. It weeds out all of your Units that cannot "hook" into this command, and it returns a Table, based on a search of a table, "cmdID". I'm still a little confused, but basically, it sorts stuff out, so that we can use it. In theory, you only need ONE of these, along with that first search for Names, for ALL Units that use custom commands. The Functions listed above this are different, and need to be unique, but this is generic sorting code, for preventing your code from executing when it shouldn't. Lastly, this code is checking a list of the available "hooks", and, if present, it <> nil.

Here's the list of available "hooks"- a simple config, formatted as a Table, basically, tying the very specific Unit Name to a very specific Command, which is, in turn, tied to a very specific Icon:

Code: Select all

AllowHooks = {
        [Cmds.NewbieCommand]=AllowNewbieCommand
}
Lastly, we have this code, which, again, uses a Table approach, and allows us to invoke the very specific command we've written:

Code: Select all

function CommandFallback(unitID, unitDefID, teamID, cmdID, params, options)
        -- dispatch commands
        -- in general, use tables instead of if/else if chains, except
        -- when there's a small amount of possibilities, it's cleaner *and*
        -- faster
        return CmdHooks[cmdID](unitID, unitDefID, teamID, cmdID, params, options)
end
Once again, like AllowCommand, we're going by another config, which is basically a Table:

Code: Select all

CmdHooks = {
        [Cmds.NEWBIE_COMMAND]=CmdNewbieCommand
}
So... <makes panting sounds, that was a lot of typing>

Now, we have everything except for two things, necessary to make that minelayer... er, I mean, our Newbie Command:

1. The logic invoked when CmdNewbieCommand is started by an event- in this case by a player clicking a button.

2. Disabling the command, so that it cannot be done after it executes.
Last edited by Argh on 27 Aug 2007, 05:06, edited 1 time in total.
User avatar
Argh
Posts: 10920
Joined: 21 Feb 2005, 03:38

Post by Argh »

Ah, thanks, AF! This whole Tables thing is, erm, one of the things that's driving me batty about LUA... I'm used to:

"You- you're an INTEGER, got that?"

"Yes, sah! I am an INTEGER!"

... nesting within nesting, where it must be incredibly easy to make mistakes in type and bork your script... makes my brain hurt :oops: I will probably try, rather desperately, to avoid that stuff, it will just cause Bad Things To Happen ;)
User avatar
Argh
Posts: 10920
Joined: 21 Feb 2005, 03:38

Post by Argh »

Ok, now... it's time to create a Unit (the NewbieDevice) and place it in the gameworld! Hurrah!

Code: Select all

Cmds = {
        NEWBIE_COMMAND=250
}
This gives a value to NEWBIE_COMMAND, when activated.

Code: Select all

OurCmd = {}
for _, id in pairs(Cmds) do
        OurCmd[id] = true
end
This is, so far as I can see, asking, "hey, what command is currently being issued?" This means, among other things, you could assign multiple commands to a single button- i.e., "Do A & B, if both share same numeric value from Table Cmds". Then you could have, for example, "combo attacks", with "powerup times", where some of it charges back up faster than others, or where you can only use "powerup level C", if A and B are already charged. I can see a lot of uses for that little section right there.

Code: Select all

NEWBIE_DEVICE_RANGE = 200
NEWBIE_DEVICE_RANGE_SQ = NEWBIE_DEVICE_RANGE * NEWBIE_DEVICE_RANGE
Here is, for about the only time in this whole script, what looks like some very ordinary variable assignments. I should probably ask... are these local? Or global? But, tbh, they're invoked in only one place, under very specific circumstances, so it's not a major worry.

Code: Select all

_NewbieCommandUsed = {}
Here, we're creating a Table, which we will assign variables to within the main body of the script. Basically, all I'm using it for is to halt the script, if invoked more than once.



Ok, so now we have a command assignment, LUA knows when this thing is being invoked, we've set up some basic commands and a table, for storing variables specific to the executing Unit. Time to actually do something.

Code: Select all

function CmdNewbieCommand(unitID, unitDefID, teamID, cmdID, params, options)

-- defines the Newbie Command, so that it doesn't try to check a nil value
        if _NewbieCommandUsed[unitID] == nil then
                _NewbieCommandUsed[unitID] = 0
        end

        -- if we've invoked the Newbie Command before, do nothing
        if _NewbieCommandUsed[unitID] > 0 then
                return true;
        end

        -- XXX need unsafe changes, we're doing something potentially nasty
        AllowUnsafeChanges("USE AT YOUR OWN PERIL")

        -- try to create in map
        local z, facing

        -- name, X, Y, Z, facing, team (optional) -> unitid
        local newID = Spring.CreateUnit('NewbieDevice', params[1], params[2], z, facing)
        -- This will create the Unit, and put it on the ground.
        Spring.SetUnitPosition(newID, params[1], params[2], params[3])


-- start a cob script, which will animate our Unit "planting" the Newbie Device
        Spring.CallCOBScript(unitID, "PlantNewbieDevice")

        -- XXX unsafe changes not needed anymore
        AllowUnsafeChanges("thanks")

-- We're informing LUA that this Unit has built a Newbie Device
        _NewbieCommandUsed[unitID] = 1
        return true
end
In the Function above, we're setting up the basics of creating the Unit. I have not figured out how to make sure that the Unit that's created is assigned to my Team yet, so for now, it's just being created, and I haven't even given it to Gaia.

Note that this just *tries* to create the Unit. It doesn't necessarily succeed, and it doesn't give the user any feedback.

Now, for some code to check whether our spawning Unit is in range, or has already fired:

Code: Select all

-- check range
-- if you want to disable a command, always check that here, merely removing
-- a button won't be sufficient
function AllowNewbieCommand(unitID, unitDefID, teamID, cmdID, params, options)
        local x, z
        x, _, z = Spring.GetUnitPosition(unitID)
        local dx = params[1]-x
        local dz = params[3]-z
        if dx*dx + dz*dz > NEWBIE_DEVICE_RANGE_SQ then
                -- XXX don't know if this echoes to all players
                Spring.Echo("Not in range to plant Newbie Device, select somewhere closer")
                return false
        end
        if _NewbieCommandUsed[unitID] = 1 then
                 Spring.Echo("Newbie Devices have been used up!")
                 return false 
        return true
end
This code basically provides the user with helpful feedback, instead of the dreaded, "WTF ARfGFH, IT BORKEN, IT SAY IT WURK". Give users feedback! They appreciate it, and it makes your game feel 100% more professional!

Now, if the Newbie Device was actually planted successfully... we want to change the command description, so users aren't confused:

Code: Select all

function UpdateNewbieCommandDescription(unitID, unitDefID, teamdID)
        local cmds = Spring.GetUnitCmdDescs(unitID)
        local i = 0
        local thecmd
        for k, cmd in ipairs(cmds) do
                if cmd.id == Cmds.NEWBIE_COMMAND then
                        i = k
                        thecmd = cmd
                        break
                end
        end
        if _NewbieCommandUsed[unitID] = 1 then
                thecmd.name = string.format("Empty!")
        else
                thecmd.name = "Newbie Command!"
        end
        Spring.EditUnitCmdDesc(unitID, i, thecmd)
        return 1
end
Which, when ya boil it down, says, "If Unit can perform NEWBIE_COMMAND, then change the Icon's text based on whether or not it ever executed successfully."


*************************************************************
*************************************************************
*************************************************************

That is, more or less, it. There are two major features missing from this code, besides whatever smallish bugs I find (or, ack, files that show up missing) and I would like some help with these, so that this sort of command, when it has a range, looks professional, and acts right:


1. I dunno how to assign the Newbie Device (in my case, a minefield I've developed) to my Team properly. Should be easy, I just don't know what to put there. In the Orbital Barrage script, the object spawned is assigned to Gaia... obviously, I don't want that to happen, in this case.

2. I need to draw a range square, since this uses a square range. I really don't have any idea how to do that yet. Any help would be appreciated, I know that's probably a lot harder, because it'd have to be contextual (i.e., "if user presses button, and _NewbieCommandUsed[unitID] = 0, show range square, until either the Unit command context changes (hard, I'm sure) or _NewbieCommandUsed[unitID] = 1, otherwise do nothing".


Feel more than free to find, and hopefully share, any other major screwups with me. Just be nice about it, I did a lot of typing here :P

Assume, for now, that this would go in main.lua- I know how includes work. The only thing that bugs me, frankly, is that the Orbital Barrage main.lua refers to "print_r.lua", but it's unclear where those Functions are being called within this example script.
User avatar
jcnossen
Former Engine Dev
Posts: 2440
Joined: 05 Jun 2005, 19:13

Post by jcnossen »

most of the code I'm seeing is very explicit about local definitions, so I suspect it's a big deal.
Yes, local variables in lua are the only variables that do not cause table accesses. Also if you're not declaring something 'local', it will be stored in the globals table, and stay there for the rest of the program.

I'm not sure if you realise it yet, but tables are used for everything, including structures and classes (if you want them).
The '.' operator is actually short for indexing a table with a string index.

Code: Select all

Spring.InsertUnitCmdDesc(unitID, 123, cmd)
Equals

Code: Select all

Spring["InsertUnitCmdDesc"](unitID, 123, cmd)
User avatar
AF
AI Developer
Posts: 20687
Joined: 14 Sep 2004, 11:32

Post by AF »

Just as

Code: Select all

my.bannanas = 5
Is the same as

Code: Select all

my["bannanas"] = 5
I could then do

Code: Select all

my.apples = Spring.Echo
my.apples("hello world")
User avatar
url_00
Posts: 163
Joined: 15 Apr 2007, 22:44

Post by url_00 »

Argh, do you use an IDE?
And, if so, what one?

Right now I'm using a wxlua.
not very good...
imbaczek
Posts: 3629
Joined: 22 Aug 2006, 16:19

Post by imbaczek »

you've got to really take care in one thing:

Code: Select all

1. if foo = bar [...]
2. if foo == bar [...]
In 1, you assign bar to foo. In 2, you compare, which is what you want 99% of the time.

Now, onto one-time commands:

Code: Select all

function AllowNewbieCommand(unitID, unitDefID, teamID, cmdID, params, options)
        if _NewbieCommandUsed[unitID] == 1 then
                 Spring.Echo("Newbie Devices have been used up!")
                 return false
        end

        local x, z
        x, _, z = Spring.GetUnitPosition(unitID)
        local dx = params[1]-x
        local dz = params[3]-z
        if dx*dx + dz*dz > NEWBIE_DEVICE_RANGE_SQ then
                -- XXX don't know if this echoes to all players
                Spring.Echo("Not in range to plant Newbie Device, select somewhere closer")
                return false
        end
        return true
end

function UpdateNewbieCommandDescription(unitID, unitDefID, teamdID)
        local cmds = Spring.GetUnitCmdDescs(unitID)
        local i = 0
        local thecmd
        for k, cmd in ipairs(cmds) do
                if cmd.id == Cmds.NEWBIE_COMMAND then
                        i = k
                        thecmd = cmd
                        break
                end
        end
        _NewbieCommandUsed[unitID] = 1
        Spring.RemoveUnitCmdDesc(unitID, i)
        return 1
end
You also need to modify BOS like this somewhere:

Code: Select all

call-script lua_UpdateNewbieCommandDescription()
You could do without LuaCOB, but you'd be unable to start an animation or delay the cast in some way.

Creating a unit in your team:

Code: Select all

        local newID = Spring.CreateUnit('NewbieDevice', params[1], params[2], z, facing) 
Add teamID passed to the function after facing argument, ie:

Code: Select all

        local newID = Spring.CreateUnit('NewbieDevice', params[1], params[2], z, facing, teamID) 
As for drawing squares/circles/whatever, it's a gadget, my knowledge is limited in their case. CA team has some and I guess they could help, maybe even they could find a way to draw the custom command when shift is pressed.
Post Reply

Return to “Lua Scripts”