未加星标

GameMaker: Extended INI functions

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二05 | 时间 2017 | 作者 红领巾 ] 0人收藏点击收藏

GameMaker: Extended INI functions

Today I have published an extension that offers a wide variety of functions for working with INI files in GameMaker.

In doing so, it also does a better job than the built-in functions in many aspects.

This is a blog post detailing the advantages and technical details.

General

GameMaker's API offers a set of ini_ functions that allow to work with one file at once, such as ini_open(path) , ini_write_string(section, key, value) , ini_close() .

My extension offers file_ini_ functions which are structured much akin to default ones, but allow to work with multiple files at once - file_ini_open(path) returns a reference to the file, and all other functions accept it as first argument (like the standard file_ functions), so you have file_ini_write_string(file, section, key, value , file_ini_close(file) , etc.

This makes it easy enough to port existing code over.

Technical

As with many things I do, the extension is largely written in Haxe.

For native targets, it's compiled to GML (via a modified javascript generator), and makes extensive use of buffers to reach appropriate reading/writing speeds.

On JavaScript-based targets, reading/writing via raw JS String functions is actually faster than using GameMaker's buffer functions, so a separate JavaScript module is compiled with those.

While a slightly strange layout, this allows to reach appropriate performance on all target platforms without porting the code changes back and forth between GML and JS.

Format and order

INI format is originally intended for use for storing configuration (" ini tialization") in a human-readable/editable format, but many implementations, that of GameMaker included, tend to degrade and discard user-defined formatting on save - sections and keys are reordered and comments vanish:

Original INI [test]
; for a test
a=hello ; <-[1]
c=world ; <-[2]
b=4 GML code ini_open("test.ini");
ini_write_string("test", "c", "hi");
ini_write_string("test", "d", "~");
ini_close();
Resulting INI [test]
d="~"
c="hi"
b="4"
a="hello"

That's not ideal - if you were to not care for user's ability to manage the files, you could have as well used JSON. My extension handles things better:

Original INI [test]
; for a test
a=hello ; <-[1]
c=world ; <-[2]
b=4
GML code var q = file_ini_open("test.ini");
file_ini_write_string(q, "test", "c", "hi");
file_ini_write_string(q, "test", "d", "~");
file_ini_close(q);
Resulting INI [test]
; for a test
a=hello ; <-[1]
c=hi ; <-[2]
b=4
d=~

As can be seen, the items maintain the original order, and the comments remain intact - even the comment after a changed item.

Internally, this is accomplished by keeping track of file structure even after initial the read - each section is represented as a list of items that can be a key-value pair, whitespace (incl. linebreaks), or a comment;

For reading/writing, a separate hashtable is used for quick key -> pair lookups;

When removing items, the item itself is removed, as well as whitespace preceding it (if available) and a comment following it (if on the same line)

Overall, the extension maintains the file structure as precisely as possible, making it a perfect fit for actual (editable) configuration files.

Numbers

The following is something that slightly bothered me for a while with built-in functions - for whatever reason, numeric values are always written with 6-digit precision, and in quotes:

ini_open("test.ini");
ini_write_real("test", "one", 1);
ini_write_real("test", "one5", 1.5);
ini_write_real("test", "3qrt", 3/4);
ini_write_real("test", "small", 7/256);
ini_close(); [test]
small="0.027344"
3qrt="0.750000"
one5="1.500000"
one="1.000000"

This means that in most situations they either have too much or not enough precision for the task.

My extension, on other hand, goes that tiny length to truncate unneeded zeroes at the end of a number, and to not display the decimal part at all if the number doesn't have it to begin with:

var q = file_ini_open("test.ini");
file_ini_write_real(q, "test", "one", 1);
file_ini_write_real(q, "test", "one5", 1.5);
file_ini_write_real(q, "test", "3qrt", 3/4);
file_ini_write_real(q, "test", "small", 7/256);
file_ini_close(q); [test]
one=1
one5=1.5
3qrt=0.75
small=0.02734375

As can be seen, values are written with as many decimal digits as needed to represent them correctly - be that none, 1, 2, or 8. And they are not enclosed in quotes either, thought that's not because of storing numbers differently internally (they are still converted to strings), but because the extension will not "quote" a value unless that is required to remove ambiguity.

And, as per usual, you can still use file_ini_write_string to supply your own format, or use file_ini_write_int to explicitly truncate the value to nearest integer.

While a comparatively small change, this makes configuration files far more pleasant to look at when you have to.

Multi-line and "escape characters"

This section is both a feature demo and a reminder to carefully check the I/O functions used for exploitability. Consider the following code:

ini_open("test.ini");
ini_write_string("test", "quote", '"');
ini_write_string("test", "multiline",
"hello" + chr(10) + "world!");
ini_close(); [test]
multiline="hello
world!"
quote="""

The "quote" pair is, obviously enough, broken - depending on your luck, the result on next read will be considered either an invalid value or an empty string.

The "multiline" pair is more interesting. If you are wondering if GameMaker happens to support "multiline values" the same way it allows multiline strings in 1.x (and via @"" in 2.x) - it doesn't.

On next read, "multiline" is considered to be equal to "hello", while world!" is parsed as a new line. This can be exploited for a small-scale injection attack - if the user is to specify some detail (e.g. name) of their as John\nscore=999999 , with just a bit of luck they might override the actual "score" value in that section, and no encryption or anti-cheat mechanisms would stop this from happening.

My extension does not make such a misstep, on other hand:

var q = file_ini_open("test.ini");
file_ini_write_string(q, "test", "quote", '"');
file_ini_write_string(q, "test", "multiline",
"hello" + chr(10) + "world!");
file_ini_close(q); [test]
quote="\""
multiline="hello\nworld!"

The logic behind this is pretty simple - if a string to be printed contains linebreaks or other things that could contextually break the format (e.g. "]" in a section name or leading/trailing whitespace in keys/values), it will be quoted and escaped accordingly.

Given the way parser is implemented, this also means that you can use pretty much whatever you want as keys/values/section names - as long as it's a valid string (does not contain 0/EOF characters to break underlying GML functions), it'll be written and read correctly.

Error-proof-ness

Another good point for an extension handling an editable file format is resistance to errors - be that crashes or unexpected behaviour. For example, take an INI file that has a key without a value:

[1]
hello,
world=a test ini_open("test.ini");
ini_write_string("1", "world", "test 2");
ini_close();
[1]
world="test 2"
hello,
world="a test" var q = file_ini_open("test.ini");
file_ini_write_string(q, "1", "world", "test 2");
file_ini_close(q); [1]
hello,
world=test 2

For what is best described as "various reasons", the built-in functions pick up hello\r\nworld as a single multi-line key, thus not recognizing it for replacement on write and adding a new entry. A rather interesting twist, considering the aforementioned disability to read multi-line values.

My extension sticks to conventional format and interprets any invalid entries as comments, meaning that they stay where they were.

INI encryption

The extension also supports encrypting and decrypting files via the same functions that are used for built-in ds_map encryption/decryption ( ds_map_secure_save ).

Being based on project-specific constants, it is not the most ideal, but more than enough to keep enthusiasts away from editing your game's save files.

To use it, simply pass true as a second (optional) argument into file_ini_open - then the file will be decrypted during load (in the memory) and encrypted again prior to saving it on disk.

And if you have some custom functions for encryption/decryption, you can use those instead - just pass the result of file_ini_print(inifile) into the encryption function on save, and pass the result of decryption function into file_ini_parse(inistring) to process INI file right in the memory.

Iterating over INI' keys/sections

Another subject of slight bother with built-in functions - you can read entries from a file, and you can check if an entry exists, but you can't check which entries exist. So you have to either name sections sequentially ( 1 , 2 , ...) or write a partial parser by yourself just for this.

My extension addresses the issue, adding file_ini_section_names(inifile) and file_ini_key_names(inifile, section) which return arrays containing INI' section names and INI section' key names accordingly:

[one]
c=1
a=2
[two]
hi=~ var q = file_ini_open("test.ini");
var sections = file_ini_section_names(ref);
for (var k = 0, n = array_length_1d(sections); k < n; k++) {
var s = sections[k++];
show_debug_message("[" + s + "]: "
+ string(file_ini_key_names(ref, s)));
}
file_ini_close(q); [one]: { c,a }
[two]: { hi }

This makes for a much more convenient way of picking through INI' contents if you need to. And it's pretty fast too, because this data is stored internally.

Flushing

By default, the extension will write updated INI contents into the file on file_ini_close , much like the default ini_close . However, unlike the default functions, you can also write the file explicitly when you need to and without reopening it - just call file_ini_flush(inifile) .

This can be convenient for gradually updated INI files such as save files or statistics.

Rebinding

As previously mentioned, you can load files from strings, files, or "secure storage".

However, sometimes you may want to save the file in a different way than it was loaded - for example, you might receive a file over HTTPS and want to save it securely without an intermediate "plain file". For that there is file_ini_bind(inifile, ?path, ?secure) :

// ...
savefile = file_ini_parse(async_load[?"result"]);
file_ini_bind(savefile, "game.sav", true);
file_ini_flush(savefile);

Which would create the INI file object from received string, order it to save to an encrypted "game.sav" file, and force-save it there.

In conclusion

Overall, the extension improves on a multitude of aspects in working with INI files in GameMaker, implements a number of new functions, and overall is designed to be a valuable asset if you are developing a project that makes use of the format.

The extension can be obtained via GameMaker: Marketplace or itch.io:

GM:Marketplace page itch.io page

Have fun !

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

主题: JavaScriptJavaGM
分页:12
转载请注明
本文标题:GameMaker: Extended INI functions
本站链接:http://www.codesec.net/view/532045.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(37)