Easy config files: Dictionary to Ini files and vice versa

Started by eri0o, Sun 13/09/2020 17:45:48

Previous topic - Next topic

eri0o

Not sure if this already exists... Did it for myself because I needed a quick way to config something independently of save games. It's an extension of AGS Dictionaries so they can serialize and deserialize to ini files and strings.

dicttoini.ash
Code: ags
// DictToIni Module Header 
// Version 0.0.1 - needs testing!
// Note: currently spaces aren't trimmed

/// Creates a new ini-like string representing the dictionary.
import String ToIniString(this Dictionary*);

/// Overwrites the dictionary with the key value pairs in the ini-like string.
import void FromIniString(this Dictionary*, String ini_string);

/// Creates an ini file on the player's save directory, from current dictionary key value pairs.
import void ToIniFile(this Dictionary*, String filename);

/// Reads an ini file on the player's save directory and replaces current dictionary with file's key value pairs.
import void FromIniFile(this Dictionary*, String filename);


dicttoini.asc
Code: ags
// DictToIni Module Script

#region STRING_EXTENSIONS
// ---- START OF STRING EXTENSIONS ---------------------------------------------

int CountToken(this String*, String token)
{
  int count = 0, cur = 0, next = 0;
  String sub = this.Copy();

  while(sub.Length > 0)
  {
    if(sub.IndexOf(token)==-1) return count;
    sub = sub.Substring(sub.IndexOf(token)+token.Length, sub.Length);
    count++;
  }
  return count;
}

String[] Split(this String*, String token)
{
  int i = 0, cur = 0, count;
  count = this.CountToken(token);
  if(count<=0)
  {
    String r[] = new String[1];
    r[0] = null;
    return r;
  }

  String r[] = new String[count+2];
  String sub = this.Copy();

  while(i < count)
  {
    cur = sub.IndexOf(token);
    if(cur==-1) cur=sub.Length;
    r[i] = sub.Substring(0, cur);
    sub = sub.Substring(sub.IndexOf(token)+token.Length, sub.Length);
    i++;
  }

  r[i] = sub.Substring(0, sub.Length);
  i++;
  r[i] = null;
  return  r;
}
// ---- END OF STRING EXTENSIONS -----------------------------------------------
#endregion //STRING_EXTENSIONS

String ToIniString(this Dictionary*)
{
  String ini_string = "";
  int keycount;
  String keys[];
  
  keys = this.GetKeysAsArray();
  keycount = this.ItemCount;
  
  for(int i=0; i<keycount; i++)
  {
    String value = this.Get(keys[i]);
    ini_string = ini_string.Append(String.Format("%s=%s\n", keys[i], value));
  }
  
  return ini_string;
}

void FromIniString(this Dictionary*, String ini_string)
{
  this.Clear();
  if(String.IsNullOrEmpty(ini_string)) return;
  
  int linecount = ini_string.CountToken("\n");
  String lines[] = ini_string.Split("\n");
  
  for(int i=0; i<linecount; i++)
  {
    if(lines[i].IndexOf("=") <= -1) continue;
   
    String kv[] = lines[i].Split("=");
    if(String.IsNullOrEmpty(kv[0]) || String.IsNullOrEmpty(kv[1])) continue;

    this.Set(kv[0], kv[1]);
  }
}

void ToIniFile(this Dictionary*, String filename)
{
  if(this.ItemCount == 0) return;
  
  String filepath = "$SAVEGAMEDIR$/";
  filepath = filepath.Append(filename);
  File* file = File.Open(filepath, eFileWrite);
  
  String ini_string = this.ToIniString();
    
  file.WriteRawLine(ini_string);

  file.Close();
}

void FromIniFile(this Dictionary*, String filename)
{
  this.Clear();  
  if(String.IsNullOrEmpty(filename)) return;

  String filepath = "$SAVEGAMEDIR$/";
  filepath = filepath.Append(filename);
  File* file = File.Open(filepath, eFileRead);
  
  for(String line = ""; !file.EOF; line = file.ReadRawLineBack())
  {
    if(String.IsNullOrEmpty(line)) continue;
    if(line .IndexOf("=") <= -1) continue;
    
    String kv[] = line.Split("=");
    if(String.IsNullOrEmpty(kv[0]) || String.IsNullOrEmpty(kv[1])) continue;
    
    this.Set(kv[0], kv[1]);
  }
  
  file.Close();  
}


I am attaching below a demo game where you can save and restore the config on the Sierra Menu. It will create a file game_config.ini in your game's save directory with the contents below (numbers depends on your slider).
Code: ini
speech_mode=1
speed=40
gamma_value=100
voice_volume=255
audio_volume=28


Demo: dicttoini.zip

Relevant change from the Sierra Template menu, is on GlobalScript.asc:

Code: ags

function btnSaveConfig_OnClick(GUIControl *control, MouseButton button)
{
  Dictionary* dict = Dictionary.Create(eNonSorted, eCaseSensitive);
  dict.Set("audio_volume", String.Format("%d",sldAudio.Value));
  dict.Set("voice_volume", String.Format("%d",sldVoice.Value));
  dict.Set("gamma_value", String.Format("%d",sldGamma.Value));
  dict.Set("speed", String.Format("%d",sldSpeed.Value));
  dict.Set("speech_mode", String.Format("%d",Speech.VoiceMode));
  dict.ToIniFile("game_config.ini");
}

function btnLoadConfig_OnClick(GUIControl *control, MouseButton button)
{
  Dictionary* dict = Dictionary.Create(eNonSorted);

  dict.FromIniFile("game_config.ini");
  if(dict.Contains("audio_volume"))
  {
    String key = dict.Get("audio_volume");
    sldAudio.Value = key.AsInt;
    System.Volume = sldAudio.Value;
  }
  if(dict.Contains("voice_volume"))
  {
    String key = dict.Get("voice_volume");
    sldVoice.Value = key.AsInt;
    SetSpeechVolume(sldVoice.Value);
  }
  if(dict.Contains("gamma_value"))
  {
    String key = dict.Get("gamma_value");
    sldGamma.Value = key.AsInt;
    System.Gamma = sldGamma.Value;
  }
  if(dict.Contains("speed"))
  {
    String key = dict.Get("speed");
    sldSpeed.Value = key.AsInt;
    SetGameSpeed(sldSpeed.Value);
  }
  if(dict.Contains("speech_mode"))
  {
    String key = dict.Get("speech_mode");
    Speech.VoiceMode = key.AsInt;
    if (Speech.VoiceMode == eSpeechVoiceOnly) btnVoice.Text = "Voice only";
    else if (Speech.VoiceMode == eSpeechTextOnly) btnVoice.Text = "Text only";
    else btnVoice.Text = "Voice and Text";
  }
}


Monsieur OUXX

Possible alternative to this module:
IniFile2: https://www.adventuregamestudio.co.uk/forums/index.php?msg=627432

- "Easy config files" is not necessarily better but relies on the more modern "Dictionary" feature of AGS 3.6+
- "IniFile2" follows more closely the aging syntax of Windows' .ini files (comments, sections, etc.)


By the way, this thread should be in "Modules and plugins" rather than "Advanced Technical Forum"
 

eri0o

Oh, right, the code was quite small and I just did it to myself, and because no one said anything it didn't felt like anyone cared xD

But thinking about this again, in theory AGS engine has the ini parsing code already in it, perhaps there is some smart way to create an AGS dictionary from an INI file and also to serialize a dictionary to an ini file, like perhaps using either entry or category.entry as keys and then reading back values, like something to add in the API.

Monsieur OUXX

#3
Quote from: eri0o on Fri 19/07/2024 00:52:10I just did it to myself
After looking more closely at IniFile2, it's not exactly written in the same spirit -- so I would say the two modules are complementary. Plus, as I wrote, yours shows the way to "modern" approaches. No regrets!

Quote from: eri0o on Fri 19/07/2024 00:52:10perhaps there is some smart way to create an AGS dictionary from an INI file and also to serialize a dictionary to an ini file

Just my two cents:
Spoiler
If you want to make String-manipulation and String-parsing lovers happy, I think you should instead focus on creating String.Split(delimiter), Array.Sort, Array.Min, Array.Max (At least for primitive types: int, float, char, String -- case-sensitive and case-insensitive...). And (if at all possible!) make String.Char faster (for reading) and String.Append faster (for writing).

I would never have suggested that before, but now that you've created pseudo-properties on dynamic arrays (.Length), it's opening a world of possibilities.

Side Note: It will not only allow new powerful parsing capabilities, but Array.XXX will probably help a lot in pixel-manipulation modules too, which are hindered by the scripting language's speed. That's how Python and GDScript worked around the speed problem: by wrapping fast c++ methods and exposing them to the scripting API.
[close]
 

eri0o

String.Split (and join and trim) are already in ags4. The string things were  sped up in ags3 since 3.6.1 - I remember indexing of chars got a 12x speed, I don't remember the rest.

I think if one is using dictionaries they can get away without sorting because dictionaries can be sorted.

I don't know about the pixel stuff, the pixel stuff is more complicated, the approach we tried with direct memory access didn't do significant improvements - it was like 30% faster but this was still slow so we didn't go that road. Here the test with direct pixel array manipulation (https://github.com/adventuregamestudio/ags/issues/1997#issuecomment-1537151745), this is direct manipulation of memory, but somehow was still slow.

Crimson Wizard

Quote from: eri0o on Fri 19/07/2024 17:06:20the approach we tried with direct memory access didn't do significant improvements - it was like 30% faster but this was still slow so we didn't go that road.

I must clarify: this was not implemented not because it was not much faster, but because that would break certain things, as noted in following comments in the ticket. This, or certain alternate approach, still may be done at some point in ags4, maybe.

SMF spam blocked by CleanTalk