Undo/Redo has been implemented with a stage stack and a stage pointer.
Check out the new GitHub wiki page.
First let me say more about what was done first.
Drag and drop was already implemented but I had to make a new trait which VASSAL users will recognize as the Marked When Moved trait. I call it MarkMoved. This trait is basically a mask with an offset relative to the counter. The MarkMoved trait inherits a basic Mask trait as shown below in traits.luau
Mask = {}
function Mask:new (mask, x, y)
local o = {}
setmetatable(o, self)
self.__index = self
--- fields
o.mask = mask
o.offsetx = x
o.offsety = y
return o
end
MarkMoved = {}
function MarkMoved:new (x, y)
local o = Mask:new("_Moved", x, y)
setmetatable(self, {__index = Mask})
self.__index = self
o.moved = false
o.menuname = "Moved"
o.menuclick = "actionMoved"
o.movetrigger = function(id) Counter:markMoved(id) end
return o
end
This is how inheritance works in Lua. MarkMoved creates a Mask with _Moved as the image and offsets x and y. In addition MarkMoved needs its own fields to tell if the mask is active (moved) and a trigger for what shall be done when it receives a move call from the C++ part (function movetrigger). Besides this it needs the usual menuname and menuaction for the counterâs right-click menu. The menuaction flips the counterâs move status.
The _Moved is an index to the image resources.
string file = Counter::resources["_Moved"];
Both system resources (inherent images of the engine) and resources loaded from the module (the images created by the module developer) will be indexed in the same way with the convention that system resources have an underline.
If you look at the definition of a counter in counter.luau
Counter = {}
function Counter:new (images, x, y)
local o = Traits:new(images)
setmetatable(o, Counter)
Counter.__index = Counter
o.degrees = 0
o.x = x
o.y = y
--table.insert(Counter, o)
create_class(images[1], x, y)
State:add(images[1], Counter:copy(o))
return o
end
you will see fields for x and y position. Itâs probably a good idea to move this out to a new trait called Position. Besides x, y coordinates this trait can give the position in a stack and also the position (location) within a hex. A module developer can f.ex. add a custom field for what floor the counter is on.
In module.luau where counters are created you will now see this:
-- create two counters
Counter["12R-51R"] = Counter:new({"12R-51R", "12R-51R_f"}, 130, 100)
Counter:addTrait("12R-51R", "MarkMoved", MarkMoved:new(72, 15))
Counter["BRIGADE-3"] = Counter:new({"BRIGADE-3", "BRIGADE-3_f"}, 250, 100)
I have made a special addTrait function that can add traits individually to counters. â12R-51Râ has a MarkMoved trait while âBRIGADE-3â does not have one. Also note the calculated offsets 72 and 15. This presumes all counters are of the same size. It is possible to let these offsets be calculated as a function of the counter size.
In the C++ part a very important change was necessary. A counter has to be generated from a base image to its current image as defined by the state of the counter. The state of a counter in C++ is for the moment defined by these five fields in a state struct.
struct State
{
int x;
int y;
bool moved; // only true if trait
int degrees; // only not zero if trait
std::string name; // name of current (flipped) image
};
Every instance of a counter-class has a function to generate it visual appearance:
void Counter::setImage()
See the full code on GitHub. I also made some changes to the rotate function but âunexpected resultsâ may occur when moving rotated counters. It will be rewritten with the new GUI library. Note that struct State is updated from the Luau part, not the other way around.
counter.luau
function Counter:generateGUI()
for k, v in pairs(Counter) do
if type(k) == "string" and k ~= '__index' and type(v) ~= 'function' then
local name = Counter[k].Image.images[ Counter[k].Image.imageIndex ]
generateGUI(k,
Counter[k].x,
Counter[k].y,
Counter[k].MarkMoved ~= nil and Counter[k].MarkMoved.moved,
Counter[k].degrees,
name)
end
end
end
luau.cpp
static int generateGUI(lua_State *L)
{
const char *name = lua_tostring(L, -1);
int degrees = lua_tonumber(L, -2);
bool moved = lua_toboolean(L, -3);
int y = lua_tonumber(L, -4);
int x = lua_tonumber(L, -5);
const char *id = lua_tostring(L, -6);
lua_pop(L, 6);
Counter *found = Counter::findObj(id);
if (found)
{
found->state.x = x;
found->state.y = y;
found->state.moved = moved;
found->state.degrees = degrees;
found->state.name = string(name);
found->setImage();
}
else
printf("error\n");
return 0;
}
A game state is just the value of all fields in all traits in all counters at a given instance.
A move brings the game from one state to the next state.
Moves are stored on the stageStack.
A stage is a number of moves that brings the game state from on meaningful instance to the next meaningful instance. An example of this is moving a counter. This consists of two moves, one updating the x field and the other updating the y field. It would be meaningless to undo the x and y coordinate independently of each other.
Moves on the stageStack are separated with a special âendâ move. The âendâ move separates stages.
The stagePointer points at the current stage, the stage that is visible on the screen.
An Undo will move the stagePointer back to the previous âendâ. A Redo will move the stagePointer forward to the next âendâ (if there are any).
This is the basic idea behind the implementation of Undo and Redo.
In order to generate the game state at n - 1, it is necessary to update all the fields from
the base state up until stage n - 1. The implementation of this is very simple in Lua.
function State:executeUntil(stage)
for i = 1, stage do
local id = State.stageStack[i].id
local trait = State.stageStack[i].trait
local key = State.stageStack[i].key
local value = State.stageStack[i].value
if trait == 'Counter' then
Counter[id][key] = value
end
if trait == 'MarkMoved' then
Counter[id][trait][key] = value
end
if trait == 'Image' then
Counter[id][trait][key] = value
end
if trait == 'Delete' then
Counter[id] = nil
end
end
end
Loop through all moves from index one the the desired stage and update the fields in the traits. When this is done itâs just a matter of generating the GUI (the visible representation of the state).
function State:undo()
local i = State:findLastStage ()
State:reset() -- reset Counter with saved State
State:executeUntil(i) -- execute moves up to last stage
Counter:generateGUI()
end
Next up is finding and making a new library that will replace Gtk3.
Then itâs time to deal with stacking. First drag and drop must be implemented with the new library.
Then it must be possible to drag copies of counters from the counter window to the map window.
A grid must be set up and counters must be able to be stacked with an given offset.
Then it must be possible to open a stack and do two things: select some units (SHIFT key)
and reorder units.
I will follow the usual conventions. Hoovering over the stack will display a popup window with its contents. Double clicking a stack will open a window where selecting and reordering will be possible.
I do not think itâs a good idea to open several stacks at the same time. That clutters the board. But exactly how this is done depends on practical considerations. It is important to avoid clutter but also important to make it as user friendly as possible.
Regarding the new library, I will look closely at what I need. It must be possible to place a GUI element at x, y on the screen. It must be possible to change the z-order of GUI elements if they happen to lie on top of each other.
It is a plus (but maybe not that important) that all functions in the library that can read/write the host file system be removed. Reading/writing files is done only with C++.
I think it is good design practice to make the module as self-contained as possible. By this I mean that it should have all the needed libraries supplied. It should not be necessary for the user to install any extra software.
This means distributing the Luau VM (Virtual Machine) and the GUI library with game engine.
Of course, since many libraries are dynamic libraries, âself-containedâ is a relative notion.
But the rule still holds, a user should not have to install any software to run the module.
The user just downloads the module (which is a combination of the implementation of a game, the game engine and the GUI library) and can run it straight away.
The downside of this is that the size of a module is greater than the size of a typical VASSAL module. Let us look at what the size may be.
The size of the Gtk3 library (libgtk-3.so.0.2404.29) my Linux distribution is just 8,5 MB. This is a dynamic library. The actual needed size is larger but I guess not by much. Among other libraries GTK3 needs gdk-pixbuf (libpixman-1.so ?) which is just 694,4 kB and Cairo (libcairo.so.2.11600.0) which is just 1,2 MB. On a Linux system these libraries are already
installed and need not be redistributed.
The size of the Luau executable is 10,5 MB.
When I link I use these static libraries:
libluauvm.a 2,3 MB
libluaucompiler.a 7,7 MB
libisocline.a 1,0 MB
libluauast.a 4,9 MB
The total Luau size is then about 26 MB.
Total size of Luau and libraries is then roughly 26 MB + 10 MB or 36 MB.
The compiled size of the game engine is right now 6,7 MB but will of course increase. Finally there are the image resources. My WW1 VASSAL module has images resources of 6,7 MB. Large VASSAL modules may have image resources of 20 MB. It varies greatly.
A typically self-contained module will then be around 50 MB which is of course
much compared with a typical VASSAL module.
The downside of self-contained modules is that a lot of redundant software is distributed.
But with todayâs bandwidth and storage capacity this should not really be a problem. 40
downloaded modules is still just 2 GB, which is nothing on disks with a 500 GB or 1000 GB
capacity.
The upside of such a distributions is that the module never becomes obsolete. As long as
an opponent has the same distribution the module will always work. There is nothing outside the module that will make it stop working.
The C++ part (and GUI library) of the engine can be changed independently of the Luau part as long as the Luau C API remains the same. This is useful for the inevitable correction of bugs and new features. Only if changes imply a change of script must the user download a new version of the module. But the old module will still run with its version of binary, script and library.
A distribution of this game engine is an independently compiled C++ binary with a GUI library together with a VASSAL-like module for any particular game. The module will contain the image resources and the Luau script files for a particular game. The Luau script files are based on a template (the Luau part of the engine). They are then modified/developed to implement a particular game.
Regarding integrity, the script files can be verified before they are compiled with a checksum. The sum can be compared with a server checksum. The C++ part will have a build version for checking.
When stacking is done itâs time to piece it all together. The pilot tests I have made will be merged into a very rough pilot game engine. This in itself will demand more development.
After the game engine is in place, a very interesting phase starts. I will try to implement my WW1 Verdun VASSAL module on the new engine. How will it go? How will I be able to implement those artillery fire tables? It will be a good test on how the engine copes. It will bring into focus what more development is needed.
One very important issue is map scaling. The map and counters must be scaled independently and placed relative to each other.