7 Commits
picks1 ... ci

Author SHA1 Message Date
b580ff77b3 Disable the builds that work 2023-01-21 15:51:46 -05:00
52a3df2d3b Enable the MinGW build 2023-01-21 15:50:32 -05:00
318050ecdc Temporarily disable activation on PR 2023-01-21 15:49:37 -05:00
57a6901dab CI: Enable the mac-scons build 2023-01-21 15:48:56 -05:00
6058e0e978 scons: Missed some things in the Python2to3 update 2023-01-21 15:46:46 -05:00
1117b27f5d Add some new features to our quoted string parser, and a unit test
- Now supports \n \t \f escape sequences
- Now supports strings with literal tabs
2023-01-21 15:44:48 -05:00
105a7efa96 Add a file documenting the structure of the save format 2023-01-21 14:45:03 -05:00
10 changed files with 332 additions and 155 deletions

View File

@@ -4,111 +4,130 @@
on: {
push: {
branches: [ master, ci ]
},
pull_request: {
branches: [ master ]
}
},
jobs: {
macos-xcode: {
runs-on: macos-10.15,
env: {
DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
},
steps: [
{
name: checkout,
uses: actions/checkout@v2,
with: { submodules: true }
},
{
name: install Boost,
run: brew install Boost
},
{
name: install SFML,
run: ./.github/workflows/scripts/mac/install-sfml.sh
},
{
name: patch Xcode project,
run: ./.github/workflows/scripts/mac/fix-xcode-proj.sh
},
{
name: build,
run: ./.github/workflows/scripts/mac/xcode-build.sh
},
{
name: unit tests,
run: ./.github/workflows/scripts/mac/run-tests.sh
}
]
},
# macos-scons: {
# runs-on: macos-10.15,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
#macos-xcode: {
# runs-on: macos-10.15,
# env: {
# DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
# },
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
# with: { submodules: true }
# },
# {
# name: install dependencies,
# run: brew install scons SFML Boost
# },
# {
# name: build and unit test,
# run: ./.github/workflows/scripts/mac/scons-build.sh
# }
# ]
# },
win-vs32: {
# },
# {
# name: install Boost,
# run: brew install Boost
# },
# {
# name: install SFML,
# run: ./.github/workflows/scripts/mac/install-sfml.sh
# },
# {
# name: patch Xcode project,
# run: ./.github/workflows/scripts/mac/fix-xcode-proj.sh
# },
# {
# name: build,
# run: ./.github/workflows/scripts/mac/xcode-build.sh
# },
# {
# name: unit tests,
# run: ./.github/workflows/scripts/mac/run-tests.sh
# }
# ]
#},
#macos-scons: {
# runs-on: macos-10.15,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
# with: { submodules: true }
# },
# {
# name: install dependencies,
# run: brew install scons SFML Boost
# },
# {
# name: build and unit test,
# run: ./.github/workflows/scripts/mac/scons-build.sh
# }
# ]
#},
#win-vs32: {
# runs-on: windows-2019,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
# with: { submodules: true }
# },
# {
# name: install dependencies,
# run: '.\.github\workflows\scripts\win\install-deps.bat x86'
# },
# {
# name: build,
# run: '.\.github\workflows\scripts\win\msvc-build.bat x86'
# }
# ]
#},
#win-vs64: {
# runs-on: windows-2019,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
# with: { submodules: true }
# },
# {
# name: install dependencies,
# run: '.\.github\workflows\scripts\win\install-deps.bat x64'
# },
# {
# name: build,
# run: '.\.github\workflows\scripts\win\msvc-build.bat x64'
# },
# {
# name: unit tests,
# run: '.\.github\workflows\scripts\win\run-tests.bat'
# }
# ]
#},
#win-scons: {
# runs-on: windows-2019,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
# with: { submodules: true }
# },
# {
# name: install build dependencies,
# run: 'vcpkg install libxml2 && pip install scons'
# },
# {
# name: install dependencies,
# run: '.\.github\workflows\scripts\win\install-deps.bat x64'
# },
# {
# name: build and unit test,
# run: '.\.github\workflows\scripts\win\scons-build.bat'
# }
# ]
#},
win-mingw: {
runs-on: windows-2019,
steps: [
{
name: checkout,
uses: actions/checkout@v2,
with: { submodules: true }
},
{
name: install dependencies,
run: '.\.github\workflows\scripts\win\install-deps.bat x86'
},
{
name: build,
run: '.\.github\workflows\scripts\win\msvc-build.bat x86'
}
]
},
win-vs64: {
runs-on: windows-2019,
steps: [
{
name: checkout,
uses: actions/checkout@v2,
with: { submodules: true }
},
{
name: install dependencies,
run: '.\.github\workflows\scripts\win\install-deps.bat x64'
},
{
name: build,
run: '.\.github\workflows\scripts\win\msvc-build.bat x64'
},
{
name: unit tests,
run: '.\.github\workflows\scripts\win\run-tests.bat'
}
]
},
win-scons: {
runs-on: windows-2019,
steps: [
{
name: checkout,
uses: actions/checkout@v2,
with: { submodules: true }
with: { submodules: true }
},
{
name: install build dependencies,
@@ -120,53 +139,31 @@
},
{
name: build and unit test,
run: '.\.github\workflows\scripts\win\scons-build.bat'
run: scons toolset=mingw
}
]
},
# win-mingw: {
# runs-on: windows-2019,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
#linux: {
# runs-on: ubuntu-20.04,
# steps: [
# {
# name: checkout,
# uses: actions/checkout@v2,
# with: { submodules: true }
# },
# {
# name: install build dependencies,
# run: 'vcpkg install libxml2 && pip install scons'
# },
# {
# name: install dependencies,
# run: '.\.github\workflows\scripts\win\install-deps.bat x64'
# },
# {
# name: build and unit test,
# run: scons toolset=mingw
# }
# ]
# },
linux: {
runs-on: ubuntu-20.04,
steps: [
{
name: checkout,
uses: actions/checkout@v2,
with: { submodules: true }
},
{
name: install dependencies,
run: 'sudo apt-get install scons libxml2-utils zlib1g libsfml-dev libboost-all-dev zenity'
},
{
name: install TGUI,
run: 'sudo ./.github/workflows/scripts/linux/install-tgui.sh'
},
{
name: build and unit test,
run: CCFLAGS=-fdiagnostics-color=always scons
}
],
}
# },
# {
# name: install dependencies,
# run: 'sudo apt-get install scons libxml2-utils zlib1g libsfml-dev libboost-all-dev zenity'
# },
# {
# name: install TGUI,
# run: 'sudo ./.github/workflows/scripts/linux/install-tgui.sh'
# },
# {
# name: build and unit test,
# run: CCFLAGS=-fdiagnostics-color=always scons
# }
# ],
#}
}
}

View File

@@ -2,5 +2,6 @@
export CC="$(brew --prefix llvm)/bin/clang"
export CXX="$(brew --prefix llvm)/bin/clang++"
export SDKROOT="$(xcrun --show-sdk-path)"
scons CXXFLAGS="-I/usr/local/opt/zlib/include" LINKFLAGS="-L/usr/local/opt/zlib/lib"

View File

@@ -91,10 +91,10 @@ if platform == "darwin":
/usr/include
/usr/local/include
"""), FRAMEWORKPATH=Split("""
/System/Library/Frameworks
/Library/Frameworks
%s/Library/Frameworks
""" % os.environ['HOME']))
{SDKROOT}/System/Library/Frameworks
{SDKROOT}/Library/Frameworks
{HOME}/Library/Frameworks
""".format(HOME = os.environ['HOME'], SDKROOT = os.environ['SDKROOT'])))
def build_app_package(env, source, build_dir, info):
source_name = source[0].name
pkg_path = path.join(build_dir, "%s.app/Contents/" % source_name)
@@ -111,6 +111,7 @@ if platform == "darwin":
return all(not lib.startswith(x) for x in system_prefixes)
def get_deps_for(source):
deps = subprocess.check_output(['otool', '-L', source]).splitlines()[1:]
deps = map(lambda s: s.decode('utf-8'), deps)
deps = list(map(str.strip, deps))
deps = list(filter(is_user_lib, deps))
deps = [x.split()[0] for x in deps]

View File

@@ -2,7 +2,7 @@
Import("env platform")
if str(platform) != "darwin":
print "Error: Building for", str(platform), "but trying to create a Mac install package"
print("Error: Building for", str(platform), "but trying to create a Mac install package")
env.Command('OBoE.dmg', "#build/Blades of Exile",
action='hdiutil create -fs HFS+ -volname "Blades of Exile" -srcfolder $SOURCE $TARGET')

View File

@@ -201,6 +201,7 @@
<ClCompile Include="..\..\..\test\scen_read.cpp" />
<ClCompile Include="..\..\..\test\scen_write.cpp" />
<ClCompile Include="..\..\..\test\spec_legacy.cpp" />
<ClCompile Include="..\..\..\test\string_quote.cpp" />
<ClCompile Include="..\..\..\test\tagfile.cpp" />
<ClCompile Include="..\..\..\test\talk_legacy.cpp" />
<ClCompile Include="..\..\..\test\talk_read.cpp" />

View File

@@ -77,6 +77,9 @@
<ClCompile Include="..\..\..\test\spec_legacy.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\test\string_quote.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\..\..\test\tagfile.cpp">
<Filter>Source Files</Filter>
</ClCompile>

View File

@@ -59,6 +59,7 @@
911A14031B8FAFC600900FD9 /* town_read.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C2A6EC1B8FA91400346948 /* town_read.cpp */; };
911A14041B8FB00300900FD9 /* talk_read.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C2A6EE1B8FAA8E00346948 /* talk_read.cpp */; };
911A14051B8FB00600900FD9 /* out_read.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C2A6ED1B8FA9FB00346948 /* out_read.cpp */; };
911DD995297C56F500205EBC /* string_quote.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 911DD994297C56F500205EBC /* string_quote.cpp */; };
911F2D991B98F43B00E3102E /* libCommon.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 911F2D981B98F43B00E3102E /* libCommon.a */; };
911F2D9A1B98F43C00E3102E /* libCommon.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 911F2D981B98F43B00E3102E /* libCommon.a */; };
911F2D9B1B98F43C00E3102E /* libCommon.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 911F2D981B98F43B00E3102E /* libCommon.a */; };
@@ -616,6 +617,7 @@
910BBAB50FB91A26001E34EA /* field.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = field.cpp; sourceTree = "<group>"; };
910BBAB80FB91ADB001E34EA /* message.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = message.hpp; sourceTree = "<group>"; };
910BBAB90FB91ADB001E34EA /* message.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = message.cpp; sourceTree = "<group>"; };
911DD994297C56F500205EBC /* string_quote.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = string_quote.cpp; sourceTree = "<group>"; };
911F2D981B98F43B00E3102E /* libCommon.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libCommon.a; path = lib/libCommon.a; sourceTree = "<group>"; };
911F2D9D1B98F44700E3102E /* libCommon-Party.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libCommon-Party.a"; path = "lib/libCommon-Party.a"; sourceTree = "<group>"; };
911F2DA21B98FF2300E3102E /* cursors */ = {isa = PBXFileReference; lastKnownFileType = folder; path = cursors; sourceTree = "<group>"; };
@@ -1540,6 +1542,7 @@
9176FEC51D550EFE006EF694 /* town_legacy.cpp */,
91C2A6EC1B8FA91400346948 /* town_read.cpp */,
91E381451B97671E00F69B81 /* town_write.cpp */,
911DD994297C56F500205EBC /* string_quote.cpp */,
);
name = src;
sourceTree = "<group>";
@@ -2131,6 +2134,7 @@
91430438296C0088003A3967 /* vector2d.cpp in Sources */,
91C763DD1B4EE7950086D879 /* map_write.cpp in Sources */,
91EF27731B693D3900666469 /* ter_read.cpp in Sources */,
911DD995297C56F500205EBC /* string_quote.cpp in Sources */,
91EF27751B693D4800666469 /* ter_write.cpp in Sources */,
91EF27771B693D5500666469 /* item_read.cpp in Sources */,
91EF27791B693D5F00666469 /* item_write.cpp in Sources */,

84
rsrc/saves/readme.md Normal file
View File

@@ -0,0 +1,84 @@
Saved Game Format
=================
The Blades of Exile saved game format is a gzipped tarball with a .exg extension
and the following structure:
* save/
* party.txt - Contains general info on the party.
* pc*N*.txt - Contains info on active PC _N_, which is 1-6.
* stored_pcs.txt - Contains a list of unique IDs for stored PCs. Optional.
* pc~*N*.txt - Contains info on stored PC _N_, which must be found in _stored_pcs.txt_.
* export.png - Contains graphics referenced from other files in this folder. Optional.
* scenario.txt - Contains some persistent information about towns in the scenario.
* setup.dat - Contains saved information about fields in recent towns you've visited.
* town.txt - Contains information about the current town, if the party is in down.
* townmaps.dat - Contains information about what areas of towns have been explored.
* out.txt - Contains information about the current outdoor region (a 2x2 grid of sectors).
* outmaps.dat - Contains information about what areas of the outdoors have been explored.
A party that is not in a scenario omits all the files from _scenario.txt_ down.
The _stored_pcs.txt_ file is only included if there actually are stored PCs.
The _town.txt_ file is omitted if the party is outdoors, but _townmaps.dat_ is not.
If you have a _save_ directory with the proper structure, it can be turned into a
valid saved game with the following shell command:
tar -zcf save.exg save
(This should work on the Windows command-line too, assuming you have `tar.exe` installed
and in the `%PATH%` or current directory.)
Most of the files in the saved game use a "tag file" format, which is described below. There are four exceptions (plus export.png):
* The _stored_pcs.txt_ file is simply a list of numbers, one per line. It's essentially an index of the stored PCs for every number in this file, there will be a corresponding _pc~N.txt_.
* The _townmaps.dat_ file is simply a list of bitfields, one per line, addressing all towns in the scenario in order of definition from top to bottom. Each bitfield covers one row of one town.
* The _out.txt_ file is a straight dump of the active outdoor terrain, two consecutive 96x96 grids of numbers separated by spaces and newlines. The first set covers the terrain while the second set covers the explored flag.
* The _outmaps.dat_ file is simply a list of bitfields, several per line, addressing all outdoor sectors in order of definition from top to bottom. Each line covers one row of every sector in a single row of the outdoors grid, so for example if the outdoors is 3x3, then each row will contain 3 bitfields addressing a row of sectors (_N_,0), (_N_,1), and (_N_,2).
Tag Files
=========
The "tag file" format is very simple. A "tag file" consists of a series of "pages", separated by formfeed characters. Each page consists of a series of "tags", one per line. Each tag consists of an "identifier" followed by zero or more "values", separated by whitespace. Values can be quoted if they contain spaces or tabs. The order of tags is preserved and can be significant. Sometimes the mere presence of a tag conveys a meaning tags without values are often used for this purpose. There are no restrictions on what characters can appear in an identifier, other than whitespace (quotes are not parsed in the identifier).
When parsing tag values, quotes (" and ') only have a special meaning if they appear as the first character. So, for example, the string "don't" would not need to be quoted, as it doesn't start with '. The following backslash escape sequences are supported in quoted strings:
* `\\` - Literal backslash
* `\'` - Literal single quote (only required in single-quoted strings)
* `\"` - Literal double quote (only required in double-quoted strings)
* `\n` - A newline
* `\t` - A horizontal tab (not required, but parsed for consistency)
* `\f` - A formfeed
Note that no escape sequences will be expanded in unquoted strings.
For tag values that are interpreted as booleans, the strings "true" and "false" are used.
Most tag values interpreted as numbers use the decimal format, but a hexadecimal format
is also supported. Which format to use is decided by the code that loads the file.
For example, a very simple two-page tag file might look like this:
NAME "It's Me!"
LEVEL 5
HEALTH 12 34
MANA 1 45
<formfeed>
ITEM 0
NAME "Broken Sword"
DAMAGE 2 7
EQUIPPED
<formfeed>
ITEM 1
NAME "Healing Potion"
ABILITY heals
An actual tag file would not add a newline between the formfeed and the following tag.
For the tag files actually used in the saved game, pages are usually identified based on
the first tag that appears in them. However, the very first page is usually treated
specially without checking what its first tag is.
The exact list of pages and tags that are used in the saved game files are not yet
finalized, so they won't be documented here. However, it is not hard to figure it out
by looking at the code look for functions called `writeTo` or `readFrom` that take
either a `cTagFile` or a `cTagFile_Page` as a parameter.

View File

@@ -128,37 +128,49 @@ std::string read_maybe_quoted_string(std::istream& from) {
from >> std::ws;
if(from.peek() == '"' || from.peek() == '\'') {
char delim = from.get();
getline(from, result, delim);
if(result.empty()) return result;
while(result[result.length() - 1] == '\\') {
result[result.length() - 1] = delim;
bool reached_end = true;
do {
std::string nextPart;
getline(from, nextPart, delim);
if(!nextPart.empty() && nextPart.back() == '\\') {
nextPart.back() = delim;
reached_end = false;
} else {
reached_end = true;
}
// Collapse any double backslashes; remove any single backslashes
for(std::string::iterator iter = nextPart.begin(); iter != nextPart.end(); iter++) {
if(iter[0] == '\\' && iter + 1 != nextPart.end() && iter[1] != '\\') {
if(iter[0] == '\\' && iter + 1 != nextPart.end()) {
iter = nextPart.erase(iter);
// After this, iter points to the second of the two backslashes, so
// when incremented by the loop, it'll point to the character after the backslashes.
// However! It might also be pointing at an n, t, or f, so substitute that if so.
switch(*iter) {
case 'n': *iter = '\n'; break;
case 't': *iter = '\t'; break;
case 'f': *iter = '\f'; break;
}
}
}
// Note that this does not support escaping the single quotes in strings delimited by double quotes, and vice versa.
result += nextPart;
}
} while(!reached_end);
} else from >> result;
return result;
}
std::string maybe_quote_string(std::string which) {
if(which.empty()) return "''";
if(which.find_first_of(' ') != std::string::npos || which[0] == '"' || which[0] == '\'') {
if(which.find_first_of(" \t\n\f") != std::string::npos || which[0] == '"' || which[0] == '\'') {
// The string contains spaces or starts with a quote, so quote it.
// We may have to escape quotes or backslashes.
int apos = 0, quot = 0, bslash = 0;
std::for_each(which.begin(), which.end(), [&apos,&quot,&bslash](char c) {
int apos = 0, quot = 0, bslash = 0, newline = 0, formfeed = 0;
std::for_each(which.begin(), which.end(), [&apos,&quot,&bslash,&newline,&formfeed](char c) {
if(c == '\'') apos++;
if(c == '"') quot++;
if(c == '\\') bslash++;
if(c == '\n') newline++;
if(c == '\f') formfeed++;
});
char quote_c;
// Surround it in whichever quote character appears fewer times.
@@ -166,15 +178,20 @@ std::string maybe_quote_string(std::string which) {
else quote_c = '\'';
// Let's create this string to initially have the required size.
std::string temp;
size_t quoted_len = which.length() + std::min(quot,apos) + bslash + 2;
size_t quoted_len = which.length() + std::min(quot,apos) + bslash + newline + formfeed + 2;
temp.reserve(quoted_len);
temp += quote_c;
for(size_t i = 0; i < which.length(); i++) {
if(which[i] == quote_c) {
temp += '\\';
temp += quote_c;
} else if(which[i] == '\\')
} else if(which[i] == '\\') {
temp += R"(\\)";
} else if(which[i] == '\n') {
temp += R"(\n)";
} else if(which[i] == '\f') {
temp += R"(\f)";
}
else temp += which[i];
}
temp += quote_c;

69
test/string_quote.cpp Normal file
View File

@@ -0,0 +1,69 @@
//
// string_quote.cpp
// boe_test
//
// Created by Celtic Minstrel on 2023-01-21.
//
#include "catch.hpp"
#include "fileio/fileio.hpp"
TEST_CASE("Quoting Strings") {
CHECK(maybe_quote_string("") == std::string("''"));
CHECK(maybe_quote_string(" ") == std::string("' '"));
CHECK(maybe_quote_string("Don't!") == std::string("Don't!"));
CHECK(maybe_quote_string("\"") == std::string("'\"'"));
CHECK(maybe_quote_string("'") == std::string("\"'\""));
CHECK(maybe_quote_string("-\"-") == std::string("-\"-"));
CHECK(maybe_quote_string("-'-") == std::string("-'-"));
CHECK(maybe_quote_string("Hello World") == std::string("'Hello World'"));
CHECK(maybe_quote_string("It's great!") == std::string("\"It's great!\""));
CHECK(maybe_quote_string("That is a \"silly\" idea.") == std::string("'That is a \"silly\" idea.'"));
CHECK(maybe_quote_string("1\n2") == std::string("'1\\n2'"));
CHECK(maybe_quote_string("1\t2") == std::string("'1\t2'"));
CHECK(maybe_quote_string("1\f2") == std::string("'1\\f2'"));
CHECK(maybe_quote_string("foo\"") == std::string("foo\""));
CHECK(maybe_quote_string("foo'") == std::string("foo'"));
CHECK(maybe_quote_string("That|is|great") == std::string("That|is|great"));
CHECK(maybe_quote_string("==!==") == std::string("==!=="));
CHECK(maybe_quote_string("Hello") == std::string("Hello"));
CHECK(maybe_quote_string("123") == std::string("123"));
CHECK(maybe_quote_string(".") == std::string("."));
CHECK(maybe_quote_string("path\\to\\file") == std::string("path\\to\\file"));
CHECK(maybe_quote_string("'path\\to\\file'") == std::string("\"'path\\\\to\\\\file'\""));
CHECK(maybe_quote_string("Can't stumble with \"quotes\" of both types!") == std::string("'Can\\'t stumble with \"quotes\" of both types!'"));
CHECK(maybe_quote_string("This is a \"complicated\" string\nwith 'many' different things to \\escape\\ in it! Shouldn't be too hard...?") == std::string("\"This is a \\\"complicated\\\" string\\nwith 'many' different things to \\\\escape\\\\ in it! Shouldn't be too hard...?\""));
}
static std::string unquote_string(std::string str) {
std::istringstream is(str);
return read_maybe_quoted_string(is);
}
TEST_CASE("Unquoting Strings") {
CHECK(unquote_string("''") == std::string(""));
CHECK(unquote_string("' '" ) == std::string(" "));
CHECK(unquote_string("Don't!") == std::string("Don't!"));
CHECK(unquote_string("'\"'") == std::string("\""));
CHECK(unquote_string("\"'\"" ) == std::string("'"));
CHECK(unquote_string("-\"-") == std::string("-\"-"));
CHECK(unquote_string("-'-") == std::string("-'-"));
CHECK(unquote_string("'Hello World'") == std::string("Hello World"));
CHECK(unquote_string("\"It's great!\"") == std::string("It's great!"));
CHECK(unquote_string("'That is a \"silly\" idea.'") == std::string("That is a \"silly\" idea."));
CHECK(unquote_string("'1\\n2'") == std::string("1\n2"));
CHECK(unquote_string("'1\t2'") == std::string("1\t2"));
CHECK(unquote_string("'1\\t2'") == std::string("1\t2"));
CHECK(unquote_string("'1\\f2'") == std::string("1\f2"));
CHECK(unquote_string("foo\"") == std::string("foo\""));
CHECK(unquote_string("foo'") == std::string("foo'"));
CHECK(unquote_string("That|is|great") == std::string("That|is|great"));
CHECK(unquote_string("==!==") == std::string("==!=="));
CHECK(unquote_string("Hello") == std::string("Hello"));
CHECK(unquote_string("123") == std::string("123"));
CHECK(unquote_string(".") == std::string("."));
CHECK(unquote_string("path\\to\\file") == std::string("path\\to\\file"));
CHECK(unquote_string("\"'path\\\\to\\\\file'\"") == std::string("'path\\to\\file'"));
CHECK(unquote_string("'Can\\'t stumble with \"quotes\" of both types!'") == std::string("Can't stumble with \"quotes\" of both types!"));
CHECK(unquote_string("\"This is a \\\"complicated\\\" string\\nwith 'many' different things to \\\\escape\\\\ in it! Shouldn't be too hard...?\"") == std::string("This is a \"complicated\" string\nwith 'many' different things to \\escape\\ in it! Shouldn't be too hard...?"));
}