Author Topic: Progress Report on Lua Conversion  (Read 4098 times)

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Progress Report on Lua Conversion
« on: February 08, 2011, 09:28:10 PM »
I reported earlier that GH2 compiles with Lua attached. Basic scripting is now working- I haven't added any in-game functions to Lua yet so I can only output to the console, but if you use or apply a skill to a prop the correct message will be printed.

Now, some big questions- how should I let Lua access the Pascal data structures? My first thought was to be clever and allow gears to be accessed either by their pointer or by their NarrativeID. If the function gets handed a lightuserdata, that's a pointer, and if it's a number then it's an ID. However, it seems to me that this could get confusing... and dangerous. There's no good way for the engine to tell if a pointer passed from Lua is valid or not. Using NarrativeIDs to reference gears has a number of advantages but the big disadvantage that you need to search for the gear you want every time you access it.

Next question: Variable handling. The Lua state doesn't get saved to disk with the save file, so regular Lua variables will be lost. I could add some functions to store the variables as attributes of gears, but that seems like a PITA. Alternatively I could provide all gears with a variables table where the values you need to keep can be stored, and then use a Lua function to output the contents of this table to the save file. The problem with this approach is that the variables need to be looked up from a table within a table, giving them unwieldy names like "gh[selfid].vars[HaveEatenLunch]".

I'm hoping to get doors (and maybe even scenes) working today. With any luck I'll be able to make a public release of an empty game soon.

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #1 on: February 09, 2011, 06:17:24 AM »
Doors are now working. Woohoo! For comparison, here's the original door script in the homebrew language:

Code: [Select]
name <Door>

USE <ifG -99 MetaPass else GoCloseDoor  if= StatVal STAT_Lock 0 else GoDoorLocked Print 1  SetStat STAT_Pass 0  Transform 1>

GoDoorLocked <Print 6>

GoCloseDoor <Print 2 SetStat STAT_Pass -100  Transform 2>

NPCOpenDoor <SetStat STAT_Pass 0  Transform 1>

roguechar <+>

roguechar1 <->

roguechar2 <+>

SDL_Sprite <meta_terrain.png>

CLUE_CODEBREAKING <if# StatVal STAT_Lock 0 else GoNotLocked Mental ifUCodeBreaking StatVal STAT_Lock else GoNoUnlock Print 3 SetStat STAT_Lock 0>

GoNoUnlock <Print 4>

GoNotLocked <Print 5>

REVEAL <DrawTerr SelfX SelfY TERRAIN_THRESHOLD Print 7>

Msg1 <You open the door.>

Msg2 <You close the door.>

Msg3 <You unlock the door.>

Msg4 <You do not manage to unlock the door.>

Msg5 <The door does not appear to be locked.>

Msg6 <The door is locked.>

Msg7 <You find a secret door!>

And here is the equivalent code in Lua:

Code: [Select]
function proto_door.use( selfptr )
-- Gonna use this door. The exact effect is going to depend on
-- whether this door is open or closed already. We can check this
-- via the door's STAT_PASS stat.
if gh_getstat( selfptr , STAT_PASS ) < -99 then
-- The door is closed. Check to see if it's locked as well.
if gh_getstat( selfptr , STAT_LOCK ) == 0 then
gh_print( "You open the door." )
gh_setstat( selfptr , STAT_PASS , 0 )
else
gh_print( "The door is locked." )
end
else
-- The door is currently open. Change that.
gh_print( "You close the door." )
gh_setstat( selfptr , STAT_PASS , -100 )
end
end

function proto_door.clue_codebreaking( selfptr )
-- Gonna try to unlock this door. Good luck, buddy!
-- First, check to make sure that the door is even locked...
if gh_getstat( selfptr , STAT_LOCK ) ~= 0 then
if gh_uskilltest( NAS_CODEBREAKING , STAT_CRAFT , gh_getstat( selfptr , STAT_LOCK ) ) then
gh_print( "You unlock the door." )
gh_setstat( selfptr , STAT_LOCK , 0 )
else
gh_print( "You do not manage to unlock the door." )
end
else
gh_print( "The door does not appear to be locked." )
end
end

function proto_door.reveal( selfptr )
-- The door was hidden, but has just been revealed.
-- Set the terrain in this tile to TERRAIN_THRESHOLD.
gh_drawterr( xpos( selfptr ) , ypos( selfptr ) , TERRAIN_THRESHOLD )
gh_print( "You find a secret door!" )
end

Lua documentation can be found online, if you need help understanding the code:

http://www.lua.org/docs.html

Next Lua problem- I don't know how to call a variable named function which is part of a table using the : format, so all trigger functions have to be explicitly passed their SelfID. Not a big problem but may be annoying later.

Offline magic.coding.fairy.peridot

  • Full Member
  • ***
  • Posts: 162
    • View Profile
Re: Progress Report on Lua Conversion
« Reply #2 on: February 09, 2011, 12:13:34 PM »
Now, some big questions- how should I let Lua access the Pascal data structures? My first thought was to be clever and allow gears to be accessed either by their pointer or by their NarrativeID. If the function gets handed a lightuserdata, that's a pointer, and if it's a number then it's an ID. However, it seems to me that this could get confusing... and dangerous. There's no good way for the engine to tell if a pointer passed from Lua is valid or not. Using NarrativeIDs to reference gears has a number of advantages but the big disadvantage that you need to search for the gear you want every time you access it.


This search need not be expensive - why not keep a hash table mapping IDs to pointers? Probably easiest to use Lua's tables, which as I understand it are hash tables under the hood. It might make garbage collection kind of annoying, but I think GH has manual deallocation anyway. This is especially handy if the table is going to be used only for Lua->Pascal calls anyway; then it only needs to live in the Lua world.

Quote
Next question: Variable handling. The Lua state doesn't get saved to disk with the save file, so regular Lua variables will be lost. I could add some functions to store the variables as attributes of gears, but that seems like a PITA. Alternatively I could provide all gears with a variables table where the values you need to keep can be stored, and then use a Lua function to output the contents of this table to the save file. The problem with this approach is that the variables need to be looked up from a table within a table, giving them unwieldy names like "gh[selfid].vars[HaveEatenLunch]".


What kind of variables are we talking about here? Many variables are naturally attached to a particular gear, say G. Ideally these would be referenced (from Lua) as G.HaveEatenLunch, but G.vars[HaveEatenLunch] isn't so outrageous. Does Lua provide syntactic sugar to let the latter be written as the former? I'm thinking by analogy with python, where all the attributes of an object are simply stored in a dictionary, so that G.foo is the same as G.__dict__['foo'], modulo inheritance; python provides several ways to jigger the attribute lookup process so you can do this sort of thing easily.

It may make sense to create "wrapper" objects for each gear, that is, Lua objects that look like gears to Lua scripters but actually contain the pointer and use lookup functions to propagate data between the Pascal-level gear and the Lua table. Here's an example of how to do this sort of thing in Lua: http://lua-users.org/wiki/ObjectProperties

If we're talking about global variables, well, I think it's not unreasonable to attach these to some appropriately global gear (the story, the world, something like that).

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #3 on: February 09, 2011, 04:59:28 PM »
This search need not be expensive - why not keep a hash table mapping IDs to pointers? Probably easiest to use Lua's tables, which as I understand it are hash tables under the hood. It might make garbage collection kind of annoying, but I think GH has manual deallocation anyway. This is especially handy if the table is going to be used only for Lua->Pascal calls anyway; then it only needs to live in the Lua world.

That's a good idea, and wouldn't be that hard to add. For now I've got the naive search running and we'll see how that goes- premature optimization being the root of all evil and all that.

Quote
What kind of variables are we talking about here?

Information set by scripts that ought to be saved with the save file. For example, if a plot involves two characters and the dialog options for "A" change after talking to "B", we'd need a "HaveSpokenToB" variable in the plot.

Obviously, temporary variables like loop counters can just use regular Lua variables.

Offline magic.coding.fairy.peridot

  • Full Member
  • ***
  • Posts: 162
    • View Profile
Re: Progress Report on Lua Conversion
« Reply #4 on: February 10, 2011, 08:59:31 AM »
What kind of variables are we talking about here?

Information set by scripts that ought to be saved with the save file. For example, if a plot involves two characters and the dialog options for "A" change after talking to "B", we'd need a "HaveSpokenToB" variable in the plot.

Obviously, temporary variables like loop counters can just use regular Lua variables.

It seems, then, that what's needed is a way to attach a collection of Lua variables to each gear. As I understand it, you already have a table of Lua values attached to each gear (the functions), and I think in Lua a variable can happily hold either a function or a more conventional value. On the other hand, you don't want or need to save the functions in a savegame, but you do need to save the variable values, so perhaps a separate table makes sense. In any case, what's left is just a question of making the Lua-level syntax attractive.

My inclination would still be to wrap each gear in a Lua table, so that all operations on the gear looked like door.pass = 0 or door.reveal(). There will probably be some plumbing work to do to make Lua values that correspond to Pascal-level gear properties propagate back and forth, but it gives them a very clean Lua-level interface (and the cleaner the Lua level is, the easier it is for people to add content...). In any case, plumbing or no, saving game would then just mean going through this table and saving all values that don't have a Pascal-level equivalent.

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #5 on: February 10, 2011, 06:59:21 PM »
Thought about the variables last night and I think I've been overthinking them. Yes, vars can be attached to the gear's table, then when saving the game any table entry that's a number or a string can be written to the file. Simple as that, no need for a complicated registry or anything else. The Programming in Lua book even has a nice chapter on serialization which appears to have all the code I'll need.

One problem, though: Not all kinds of variables can be written to the save file. Pointers won't remain meaningful between sessions. Functions will be filtered out by design. This isn't a problem for writing the engine, per se, but may be confusing to people writing content... although I guess the save file procedure can print an error message if an illegal type is stored as a gear variable. Also, I should try to minimize the use of pointers by Lua.

The idea of using object properties for all the gear values is a good one- I'll see about adding that code to the basic gear object. I can also add some type-dependent methods such as "isopen" and "isclosed" for the doors.

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #6 on: February 11, 2011, 05:55:11 AM »
Alright, stats now work like properties: you can say "x = pc.stat[ STAT_BODY ]" or "door.stat[ STAT_LOCK ] = 0" and it works. I spent a short time implementing this and the rest of my time figuring out exactly how it works. Unfortunately, the same thing can't be done for numeric attributes- NAtts require two keys to access a value, but the __index and __newindex methods can only pass along one. What looks like a multidimensional table is actually syntactic sugar for a table of tables. So, that sucks, but it's not a huge loss.

I'll add methods for accessing numeric attributes, probably gear.SetNAtt() and gear.GetNAtt().

Thinking things over again (always dangerous), maybe I will add a variables table called gear.v. I can add custom indexing so that you can't assign an illegal type to it. That should save a lot of headaches down the road.

Offline SharkD

  • Hero Member
  • *****
  • Posts: 1009
    • View Profile
    • Isometricland
Re: Progress Report on Lua Conversion
« Reply #7 on: February 11, 2011, 08:16:02 PM »
Would have to see actual code examples to understand whatever it is that you're talking about.

I mean actual examples, not just little scraps and bits that you think are pretty on the forum.

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #8 on: February 11, 2011, 11:48:11 PM »
Would have to see actual code examples to understand whatever it is that you're talking about.

We are talking about replacing GearHead's scripting language with Lua, and the best way to provide access from Lua scripts to the game resources. If you are not yet familiar with how GearHead works (what a gear is, what an attribute is) then you should probably learn about that first.

It would also be helpful if you could ask more specific questions. Are Peridot or I using terms that you aren't familiar with? Is something unclear?

Quote
I mean actual examples, not just little scraps and bits that you think are pretty on the forum.

Not sure why the code for the door doesn't count as an actual example, but whatever. The complete source code can be downloaded from SVN, "pluslua" branch instead of "trunk".

Edit- Because it'll probably be useful to the discussion, here is the complete gh_init.lua script as it currently exists. This script is run when the game starts up and is used to register gears in Lua, to deregister them when they get deleted, and to trigger their attached scripts.

Code: [Select]
-- Lua initialization for GearHead.

--  *****************************
--  ***   SCRIPT  FUNCTIONS   ***
--  *****************************

function xpos( gearptr )
return gh_getnatt( gearptr , NAG_LOCATION , NAS_X )
end

function ypos( gearptr )
return gh_getnatt( gearptr , NAG_LOCATION , NAS_Y )
end

--  *****************************
--  ***   TYPE  DEFINITIONS   ***
--  *****************************

proto_gear = {}
proto_gear.stat = {}

proto_stat = {}
proto_stat.ptr = 0
proto_stat.__index = function( table , key )
return( gh_getstat( table.ptr , key ) )
end

function proto_gear:new( o )
o = o or {}
setmetatable( o , self )
self.__index = self
o.stat = {}
setmetatable( o.stat , proto_stat )
return o
end
function proto_gear.use( self )
gh_print( "Using something!" )
end
function proto_gear:g()
return gh_gearg( self.ptr )
end


-- SCENES
proto_scene = proto_gear:new()
function proto_scene.nu1( self )
-- If the number of player units drops to zero, leave the scene.
if gh_numunits( NAV_DEFPLAYERTEAM ) < 1 then
gh_return();
end
end

-- METATERRAIN: DOOR
proto_door = proto_gear:new()
function proto_door.use( self )
-- Gonna use this door. The exact effect is going to depend on
-- whether this door is open or closed already. We can check this
-- via the door's STAT_PASS stat.
if gh_getstat( self.ptr , STAT_PASS ) < -99 then
-- The door is closed. Check to see if it's locked as well.
if gh_getstat( self.ptr , STAT_LOCK ) == 0 then
gh_print( "You open the door." )
gh_setstat( self.ptr , STAT_PASS , 0 )
else
gh_print( "The door is locked." )
end
else
-- The door is currently open. Change that.
gh_print( "You close the door." )
gh_setstat( self.ptr , STAT_PASS , -100 )
end
end
function proto_door.clue_codebreaking( self )
-- Gonna try to unlock this door. Good luck, buddy!
-- First, check to make sure that the door is even locked...
if gh_getstat( self.ptr , STAT_LOCK ) ~= 0 then
if gh_uskilltest( NAS_CODEBREAKING , STAT_CRAFT , gh_getstat( self.ptr , STAT_LOCK ) ) then
gh_print( "You unlock the door." )
gh_setstat( self.ptr , STAT_LOCK , 0 )
else
gh_print( "You do not manage to unlock the door." )
end
else
gh_print( "The door does not appear to be locked." )
end
end
function proto_door.reveal( self )
-- The door was hidden, but has just been revealed.
-- Set the terrain in this tile to TERRAIN_THRESHOLD.
gh_drawterr( xpos( self.ptr ) , ypos( self.ptr ) , TERRAIN_THRESHOLD )
gh_print( "You find a secret door!" )
end

-- METATERRAIN: STAIRS UP
proto_stairsup = proto_gear:new()
function proto_stairsup.use( self )
-- Gonna use the stairs...
if gh_getstat( self.ptr , STAT_DESTINATION ) ~= 0 then
gh_print( "You go up the stairs." )
gh_exit( gh_getstat( self.ptr , STAT_DESTINATION ) )
end
end

-- METATERRAIN: STAIRS DOWN
proto_stairsdown = proto_gear:new()
function proto_stairsdown.use( self )
-- Gonna use the stairs...
if gh_getstat( self.ptr , STAT_DESTINATION ) ~= 0 then
gh_print( "You go down the stairs." )
gh_exit( gh_getstat( self.ptr , STAT_DESTINATION ) )
end
end

-- METATERRAIN: ELEVATOR
proto_elevator = proto_gear:new()
function proto_elevator.use( self )
-- Not gonna use the stairs...
if gh_getstat( self.ptr , STAT_DESTINATION ) ~= 0 then
gh_print( "You board the elevator." )
gh_exit( gh_getstat( self.ptr , STAT_DESTINATION ) )
end
end

-- METATERRAIN: TRAPDOOR
proto_trapdoor = proto_gear:new()
function proto_trapdoor.use( self )
-- Unlike the other entrances on this list, trapdoors can be locked.
if gh_getstat( self.ptr , STAT_DESTINATION ) ~= 0 then
if gh_getstat( self.ptr , STAT_LOCK ) == 0 then
gh_print( "You go down the trapdoor." )
gh_exit( gh_getstat( self.ptr , STAT_DESTINATION ) )
else
gh_print( "The trapdoor is locked." )
end
end
end
function proto_trapdoor.clue_codebreaking( self )
-- Gonna try to unlock this trapdoor. Good luck, buddy!
-- First, check to make sure that the door is even locked...
if gh_getstat( self.ptr , STAT_LOCK ) ~= 0 then
if gh_uskilltest( NAS_CODEBREAKING , STAT_CRAFT , gh_getstat( self.ptr , STAT_LOCK ) ) then
gh_print( "You unlock the trapdoor." )
gh_setstat( self.ptr , STAT_LOCK , 0 )
else
gh_print( "You do not manage to unlock the trapdoor." )
end
else
gh_print( "The trapdoor does not appear to be locked." )
end
end


-- METATERRAIN: BUILDING
proto_building = proto_gear:new()
function proto_building.use( self )
-- Gonna enter this building, if it has a destination.
if gh_getstat( self.ptr , STAT_DESTINATION ) ~= 0 then
gh_print( "You enter the building." )
gh_exit( gh_getstat( self.ptr , STAT_DESTINATION ) )
end
end


-- The gh_prototypes table sorts the prototypes according to G,S descriptors
gh_prototypes = {}
gh_prototypes.default = proto_gear

gh_prototypes[ GG_METATERRAIN ] = {}
gh_prototypes[ GG_METATERRAIN ][ GS_METADOOR ] = proto_door
gh_prototypes[ GG_METATERRAIN ][ GS_METASTAIRSUP ] = proto_stairsup
gh_prototypes[ GG_METATERRAIN ][ GS_METASTAIRSDOWN ] = proto_stairsdown
gh_prototypes[ GG_METATERRAIN ][ GS_METAELEVATOR ] = proto_elevator
gh_prototypes[ GG_METATERRAIN ][ GS_METATRAPDOOR ] = proto_trapdoor
gh_prototypes[ GG_METATERRAIN ][ GS_METABUILDING ] = proto_building

gh_prototypes[ GG_SCENE ] = {}
gh_prototypes[ GG_SCENE ].default = proto_scene

gh_prototypes[ GG_METASCENE ] = {}
gh_prototypes[ GG_METASCENE ].default = proto_scene


--   **********************
--   ***   REGISTRIES   ***
--   **********************

-- The gh table contains all the loaded gears.
gh = {}
gh.mt = {}
setmetatable( gh , gh.mt )
gh.mt.__index = function( table , key )
-- If we are passed UserData, that should already have an entry here.
-- But what if we're passed a number? Use the reverse lookup table to
-- find the gear it corresponds to, and find its entry in the table.
if ( type( key ) == "number" ) and ( nid_lookup[key] ~= nil ) then
return( gh[ nid_lookup[key] ] )
end
end

-- Reverse lookup table. Given a NarrativeID, find the UserData of the
-- gear it belongs to.
nid_lookup = {}


--   **********************************
--   ***   BITS  WHICH  DO  STUFF   ***
--   **********************************

-- I admit, that's an intentionally bad title for this section. These are the
-- functions which get called by the game engine, as opposed to the functions
-- above which are used inside of Lua.


function gh_register( gearptr, gearscript )
-- Given a gear pointer and its associated script, store everything
-- in the gh table.

-- Determine the prototype for this gear.
proto = gh_prototypes.default;
if gh_prototypes[ gh_gearg( gearptr ) ] ~= nil then
if gh_prototypes[ gh_gearg( gearptr ) ][ gh_gears( gearptr ) ] ~= nil then
proto = gh_prototypes[ gh_gearg( gearptr ) ][ gh_gears( gearptr ) ]
elseif gh_prototypes[ gh_gearg( gearptr ) ].default ~= nil then
proto = gh_prototypes[ gh_gearg( gearptr ) ].default
end
end

P = proto:new()
P.ptr = gearptr
P.stat.ptr = gearptr

-- If this gear has a Narrative ID, store it in the reverse lookup table.
local nid = gh_getnatt( gearptr , NAG_NARRATIVE , NAS_NID )
if nid ~= 0 then
nid_lookup[nid] = gearptr
end

a,b = pcall( gearscript )
gh[gearptr] = P

if not a then
error( b )
end
end

function gh_deregister( gearptr )
-- Given a gear pointer, dispose of its entry in the gh table.
-- Also dispose of its reverse lookup, if appropriate.
local nid = gh_getnatt( gearptr , NAG_NARRATIVE , NAS_NID )
if nid ~= 0 then
nid_lookup[nid] = nil
end

gh[gearptr] = nil
end

function gh_trigger( gearptr, ghtrigger )
-- Given a gearptr and a trigger, see if there's a script to run, and if
-- so go do that.
local script_found = false
if gh[gearptr] ~= nil then
local tmp = gh[gearptr][ghtrigger]
if tmp ~= nil then
tmp( gh[gearptr] )
script_found = true
end--   **********************

end
return script_found
end

There are 12 cfunctions (Pascalfunctions, in this case) registered in Lua:
  • gh_gearg, gh_gears, gh_gearv: Return the G, S, and V descriptors from a gear
  • gh_getstat, gh_setstat: Get and set one of a gear's stat values
  • gh_getnatt: Gets one of the numeric attribute values; forgot to add SetNAtt yet
  • gh_print: Prints a string to the game console
  • gh_uskilltest: Takes a skill, stat, and target number. Returns true if skill test passed, false if failed.
  • gh_drawterr: Changes one of the map tiles to a new terrain
  • gh_numunits: Returns the number of active units on the gameboard belonging to a particular team
  • gh_return,gh_exit: Changes the current scene
Some of these functions could probably be rewritten in pure Lua after some more low level commands get added. For now I'm just leaving them as Pascal code so I can get everything running quickly.

Edit2- And I've already noticed a bug in it. Argh.
« Last Edit: February 12, 2011, 12:06:32 AM by Joseph Hewitt »

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #9 on: February 13, 2011, 09:01:24 AM »
Because of a migraine I didn't get as much work done this weekend as hoped, but I did manage to get the save files working. Each gear object now has a variables table "v". Any numbers, strings, booleans, and tables you put there will be placed in the save file and restored when you reload. The latest SVN commit includes a very simple test of this system; the bedroom in RANCON_BuildingFiller.txt has a script which prints one message the first time it's activated and a different message the second time. Since this bedroom is always generated in the starting scene, I thought it was a good place to put the test.

The next job is to get conversations working again. The main thing should be that conversations are easy to write- the old system was really difficult even if you understood it. We'll see.

Offline magic.coding.fairy.peridot

  • Full Member
  • ***
  • Posts: 162
    • View Profile
Re: Progress Report on Lua Conversion
« Reply #10 on: February 13, 2011, 10:47:03 AM »
Because of a migraine I didn't get as much work done this weekend as hoped, but I did manage to get the save files working. Each gear object now has a variables table "v". Any numbers, strings, booleans, and tables you put there will be placed in the save file and restored when you reload. The latest SVN commit includes a very simple test of this system; the bedroom in RANCON_BuildingFiller.txt has a script which prints one message the first time it's activated and a different message the second time. Since this bedroom is always generated in the starting scene, I thought it was a good place to put the test.

The next job is to get conversations working again. The main thing should be that conversations are easy to write- the old system was really difficult even if you understood it. We'll see.


Conversations are important enough that it may make sense to have, in addition to an easy-to-understand Lua way to write them, an interactive tool to write conversations. I'm thinking of something written in (probably) Lua that lets non-programmers easily add simple flavor text, rumours, and maybe very simple plot fragments. More sophisticated conversations (e.g. shopping, major plot turning points, etc.) would still be written in Lua; the idea is to have a tool that lets one trade generality for convenience.

This is kind of blue-sky, of course; for a first pass it makes sense to have just the raw Lua.

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #11 on: February 23, 2011, 01:06:02 AM »
Sorry for my absence- first Sean was sick, then I was sick, and we're moving to a new apartment on Friday.

Conversations are important enough that it may make sense to have, in addition to an easy-to-understand Lua way to write them, an interactive tool to write conversations.

Agreed. A tool like the Neverwinter Nights conversation editor would be great- let the user lay out the PC's lines and the NPC's responses in a tree, throw in some buttons or wizards for common tasks, and include a text editor panel for doing everything else. It seems to me that this shouldn't be too hard to program in Lazarus or any other GUI-making system.

In the meantime, maybe simple conversation tree layout can be handled by gearparser.pp. Because of the persona fragments* I was planning to model conversation nodes as individual gears up until the conversation gets inserted into an adventure, at which point in time the nodes get compiled into a Lua script. It could look something like this:

Code: [Select]
say "Have you recovered the %name5% from %name4%?"
condition <if self.v.HaveAcceptedQuest then return( true ) end>
say "I just started working as a cavalier, and already I've messed up. I was supposed to guard the %name5% but it's been stolen out from under my nose."
  prompt.accept_quest "Maybe I could help you get it back."
    say "You'd do that for me? Thanks! The thief who took it is named %name4%... I'm sorry that I don't have any more information to tell you."
    script <self.v.HaveAcceptedQuest = True;>
  prompt.deny_quest "Your troubles do not interest me."
    say "Sorry... I'll keep my problems to myself."
    script <self:AddReaction( -10 ); self:EndPlot() >
  prompt "The %name5%, huh? What's that?"
    say "\ITEM_DESC %5% \ITEM_HISTORY %5% So, can you help me recover it?"
      link accept_quest
      link deny_quest

On first glance, this doesn't look much more readable than the current conversations... let me explain what's supposed to be going on. Each "say" line lists something that the NPC will say, while each "prompt" line gives a response from the PC. Indentation is significant, with deeper indentation showing a child node. Nodes can have conditions attached to them- the first "say" node which either returns TRUE or has no condition is the one which gets executed. A "prompt" or a "say" may optionally be given a label which can then be jumped to by a "link". They may also contain scripts.

Any suggestions for making this more readable/easier to understand?

*Persona Fragment: A piece of dialog which can be used in many different conversations. For instance, several conversations start with a "*NiceToMeetYou" PFrag. See the PFRAG_*.txt files in the series directory.

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #12 on: March 06, 2011, 04:09:29 AM »
Just checking in quickly from my inlaws place to report that conversations work in the latest SVN update of the PlusLua branch. There is no easy way to create conversations yet, but they work.

On Tuesday we leave for Canada, and will arrive at my parents house by Friday. After tonight I'll be away from internet again until then.

Offline SharkD

  • Hero Member
  • *****
  • Posts: 1009
    • View Profile
    • Isometricland
Re: Progress Report on Lua Conversion
« Reply #13 on: May 03, 2011, 01:07:21 AM »
In the meantime, maybe simple conversation tree layout can be handled by gearparser.pp. Because of the persona fragments* I was planning to model conversation nodes as individual gears up until the conversation gets inserted into an adventure, at which point in time the nodes get compiled into a Lua script. It could look something like this:


The example is of a binary yes/no scenario. What would the code for a more complex situation look like? (I.e. multiple choices.)

Also, I disagree regarding the intermediary format you're considering. Might as well just start with Lua from the get-go. Lua table syntax is pretty easy to read.
« Last Edit: May 03, 2011, 01:26:17 AM by SharkD »

Offline Joseph Hewitt

  • Administrator
  • Hero Member
  • *****
  • Posts: 2552
    • View Profile
    • http://www.gearheadrpg.com
Re: Progress Report on Lua Conversion
« Reply #14 on: May 03, 2011, 01:55:11 AM »
The example is of a binary yes/no scenario. What would the code for a more complex situation look like? (I.e. multiple choices.)

the first "say" node which either returns TRUE or has no condition is the one which gets executed.