Latest Posts

Topic: Scenarios: File I/O

GunChleoc
Avatar
Joined: 2013-10-07, 15:56
Posts: 3324
Ranking
One Elder of Players
Location: RenderedRect
Posted at: 2018-04-18, 08:50

The mechanics should work for leveling out the difficulty. After leveling out the benefits, good players should still end up with a slight advantage as a reward, but not too much.


Busy indexing nil values

Top Quote
Nordfriese
Avatar
Topic Opener
Joined: 2017-01-17, 18:07
Posts: 1929
OS: Debian Testing
Version: Latest master
Ranking
One Elder of Players
Location: 0x55555d3a34c0
Posted at: 2018-04-21, 17:00

hessenfarmer wrote:

Just a short question as I don't have time to test it myself: are your changes working? Does the interface work correctly in your branch?

More or less, at the current state of the branch. If the Widelands homedir is the default location, the campaign_data.lua script works fine.
But if the user sets a different homedir (by starting Widelands with the --homedir option), this doesn´t work yet.

I´m implementing homedir as a property of EditorGameBase, so it can be accessed as wl.Game().homedir. I tried to fetch it from WLApplication::homedir_, but Lua´s io.open() can´t cope with "special" paths like "~/.widelands". I think the (Layered)Filesystem is the way to go, but it´s difficult. I´ll report when I have some success there…


Top Quote
GunChleoc
Avatar
Joined: 2013-10-07, 15:56
Posts: 3324
Ranking
One Elder of Players
Location: RenderedRect
Posted at: 2018-04-21, 18:22

I think we should have a directory under .widelands for this. Have a look at the savehandler or at how options get stored - you should be able to find some code there for picking/setting up a directory.


Busy indexing nil values

Top Quote
Nordfriese
Avatar
Topic Opener
Joined: 2017-01-17, 18:07
Posts: 1929
OS: Debian Testing
Version: Latest master
Ranking
One Elder of Players
Location: 0x55555d3a34c0
Posted at: 2018-04-22, 18:21

Completely new approach. Instead of using a Lua aux script, I have now implemented two functions of EditorGameBase in C++ :

wl.Game().save_campaign_data(campaign, scenario, text)
wl.Game().read_campaign_data(campaign, scenario)

The C++ code takes care of all file I/O and directory structures, so there´s no need to allow scripts to use io and os.

The campaign data file is saved in homedir/campaigns/campaign_name/scenario_name.wcd, for example /home/username/.widelands/campaigns/frisians/fri03.wcd. The file is human read- and editable.
The "save" function gets one string param as the text to write. It may consist of several lines. The "read" function returns these lines as one string again. "\n" is used as line separator.
If the file can´t be opened (e.g. because the user deleted it), the "read" function returns nil.

For the write access, I first tried to understand the save handler, but the packet structure seemed unnecessarily complex for writing a few lines of text. Instead, I based my code on how AI-DNA files are dumped. I´m using profile.h for the actual read/write.

As this is going rather beyond just scripting a scenario, I have created a new branch for this: lp:~widelands-dev/widelands/campaign_data.
The code finally compiles and works as expected for me.


Top Quote
hessenfarmer
Avatar
Joined: 2014-12-11, 23:16
Posts: 2646
Ranking
One Elder of Players
Location: Bavaria
Posted at: 2018-04-22, 19:14

sounds good how could the functions be assessed in the scenario?


Top Quote
Nordfriese
Avatar
Topic Opener
Joined: 2017-01-17, 18:07
Posts: 1929
OS: Debian Testing
Version: Latest master
Ranking
One Elder of Players
Location: 0x55555d3a34c0
Posted at: 2018-04-22, 22:36

A brief usage example. Let´s say you want the player to start the 4th frisian scenario with as many logs and bricks as he had when he ended the 3rd scenario.

In the third scenario, the following code is called when the scenario ends:

local number_of_logs = count_in_warehouses("log")
local number_of_bricks = count_in_warehouses("brick")
wl.Game().save_campaign_data("frisians", "fri03", "logs=" .. number_of_logs .. "\nbricks=" .. number_of_bricks)

This creates the file ~/.widelands/campaigns/frisians/fri03.wcd with the following content:

logs=81
bricks=15

The fourth scenario then gets this code near the beginning:

local text = wl.Game().read_campaign_data("frisians", "fri03")
if text == nil then
   print("The user deleted the .wcd file!")
else
   -- the value of <text> is "logs=81\nbricks=15"
   --> this can be split and processed with Lua´s string library to obtain the values:
   --> just split it once by "\n", then each of the results by "=", then you get ready-to-use key/value pairs
   ...
   local number_of_logs = ...
   local number_of_bricks = ...
   hq:set_wares({log=number_of_logs, brick=number_of_bricks})
end

Some notes:

  • All scenarios within one campaign must pass the same value for the first parameter for read and save function. It does not matter whether you call it "frisians" or "Frisians" or "FRI_Campaign" or "HelloWorld", as long as its consistent, and no other campaign uses the same name. I recommend to use the same value as the campaign_name in the campaigns.conf, that is "barbariantut", "empiretut", "atlanteans" and "frisians".
  • The save function gets the identifier of the current scenario as second parameter. I recommend the name of the scenario map minus file extension, like "fri03", "emp01", "bar02" etc. Each scenario may write only one file of data, ideally at the end! It can cheat this rule by writing several files with different identifiers like "fri03_a", "fri03_b" etc, but it shouldn´t do that.
  • Each call to save_campaign_data overwrites the data that was saved the last time the player played the scenario.
  • You can use these functions to determine whether the player already completed the current scenario before, by checking in mission fri03 whether wl.Game().read_campaign_data("frisians, "fri03") returns nil.
  • The campaign data file can contain literally any text. There is no formal syntax (as far as scripts are concerned). You can save whatever information you wish to keep, just make sure you concatenate the individual pieces of information to one string in such a way that you can extract them again in other scenarios.

I recommend as syntax that all information is stored as key/value pairs, and there is one key and one value per line, separated by "=". Having one "informal" syntax as standard helps if someone wants to read campaign data from someone else´s scenario.

I´ll test the code some more, then I´ll open a merge request…


Top Quote
hessenfarmer
Avatar
Joined: 2014-12-11, 23:16
Posts: 2646
Ranking
One Elder of Players
Location: Bavaria
Posted at: 2018-04-22, 23:06

Nice work thanks.
Off topic: If I am ready for it I would hope for your support in balancing. For the beginning you could review the excel file related to the bug regarding the training costs


Top Quote
GunChleoc
Avatar
Joined: 2013-10-07, 15:56
Posts: 3324
Ranking
One Elder of Players
Location: RenderedRect
Posted at: 2018-04-23, 10:54

The new concept looks great face-smile.png

One more idea: rather than passing a string, why not use Lua tables? This way, the data structure will remain a data structure, like this:

local campaign_data = {
   log = count_in_warehouses("log"),
   brick = count_in_warehouses("brick")
}

wl.Game().save_campaign_data("frisians", "fri03", campaign_data)

And then you can load it again with:

local campaign_data = wl.Game().read_campaign_data("frisians", "fri03")

-- This could be either nil or an empty table, it doesn't matter which way you go here
if #campaign_data == 0 then
  -- Fill the table with some default starting values
end

hq:set_wares(campaign_data)

This will need more effort on the C++ side, but make scenario scripting much easier. We could also have a table with 2 tables for wares and workers, with 2 corresponding sections in the profile.


Busy indexing nil values

Top Quote
Nordfriese
Avatar
Topic Opener
Joined: 2017-01-17, 18:07
Posts: 1929
OS: Debian Testing
Version: Latest master
Ranking
One Elder of Players
Location: 0x55555d3a34c0
Posted at: 2018-04-25, 14:03

Uploaded a new revision to the branch. Campaign data is now saved as one table. Strings, integers and booleans are permitted as values; all keys have to be strings.

Example:

wl.Game().save_campaign_data("frisians", "fri03", {
   a_key = "Hello World",
   another_key = -1,
   third_item = false,
   last_entry = 123,
})

The saving works correctly, though the table entries are saved in random order. I´m guessing this is the fault of lua_next() and that I can´t change that.

I could use some help how to return a table. Adding log() shows that the function correctly iterates and identifies keys, data types and values, but the test script shows that the return value is an empty table.

Here is my code (line 434ff), and here (line 4583ff) is the code I´m using as example for creating lua tables with specified keys and values.

What do I need to do different here?


Top Quote
GunChleoc
Avatar
Joined: 2013-10-07, 15:56
Posts: 3324
Ranking
One Elder of Players
Location: RenderedRect
Posted at: 2018-04-27, 18:45

My development machine is back in action, so I'll take a look as soon as I can.


Busy indexing nil values

Top Quote