Code Monkey home page Code Monkey logo

jomini's Introduction

CI npm size

Jomini.js

Jomini is a javascript library that is able to read and write plaintext save and game files from Paradox Development Studios produced on the Clausewitz engine (Europa Universalis IV (eu4), Crusader Kings III (ck3), Hearts of Iron 4 (hoi4), Stellaris, and others)

Aside: it's only by happenstance that this library and Paradox's own code share the same name (this library is older by several years).

Features:

  • ✔ Compatibility: Node 12+ and >90% of browsers
  • ✔ Speed: Parse at over 200 MB/s
  • ✔ Correctness: The same parser underpins a EU4 save file analyzer, and the Paradox Game Converters's ironman to plaintext converter
  • ✔ Ergonomic: Data parsed into plain javascript objects or JSON
  • ✔ Self-contained: zero runtime dependencies
  • ✔ Small: Less than 100 KB gzipped (or 70 KB when using the slim entrypoint)

Quick Start

Quick and easy way to add jomini to your project:

<body>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/index.min.js"></script>
  <script>
    jomini.Jomini.initialize().then((parser) => {
      const out = parser.parseText("foo=bar");
      alert(`the value of foo is ${out.foo}`);
    });
  </script>
</body>

Or if you want a more efficient way to get started:

<script type="module">
  import { Jomini } from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/es-slim/index_slim.min.js';

  const wasmUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/jomini.wasm';
  Jomini.initialize({ wasm: wasmUrl })
    .then((parser) => {
      const out = parser.parseText('foo=bar');
      alert(`the value of foo is ${out.foo}`);
    });
</script>

Or if Node.js is targeted or one is bundling this inside a larger application:

npm i jomini

Example

import { Jomini } from "jomini";

const data = `
    date=1640.7.1
    player="FRA"
    savegame_version={
        first=1
        second=9
        third=2
        forth=0
    }`

const parser = await Jomini.initialize();
const out = parser.parseText(data);

Will return the following:

out = {
    date: new Date(Date.UTC(1640, 6, 1)),
    player: "FRA",
    savegame_version: {
        first: 1,
        second: 9,
        third: 2,
        forth: 0
    }
}

Encoding

It's preferable to pass in raw bytes to parseText and to optionally pass in an encoding (which defaults to utf8) instead of passing in a string as this tends to be more efficient.

If passing in bytes to parseText, make sure to specify an encoding, else the strings could be deserialized incorrectly:

const jomini = await Jomini.initialize();
const data = new Uint8Array([0xff, 0x3d, 0x8a]);
const out = jomini.parseText(data, { encoding: "windows1252" });
// out = { ÿ: "Š" }

Type Narrowing

By default, jomini will attempt to narrow all values to more specific types like numbers, dates, or booleans. Type narrowing can be configured to only occur for unquoted values or disabled altogether.

const jomini = await Jomini.initialize();
const { root, json } = jomini.parseText(
  'a="01" b=02 c="yes" d=no',
  {
    typeNarrowing: "unquoted",
  },
  (q) => ({ root: q.root(), json: q.json() })
);

expect(root).toEqual({
  a: "01",
  b: 2,
  c: "yes",
  d: false,
});

expect(json).toEqual('{"a":"01","b":2,"c":"yes","d":false}');

Performance

95-99% of the time it takes to parse a file is creating the Javascript object, so it is preferable if one can slim the object down as much as possible. This is why parseText accepts a callback where one can provide JSON pointer like strings to extract only the data that is necessary.

Below shows an example of extracting the player's prestige from an EU4 save file

const buffer = readFileSync(args[0]);
const parser = await Jomini.initialize();
const { player, prestige } = parser.parseText(
  buffer,
  { encoding: "windows1252" },
  (query) => {
    const player = query.at("/player");
    const prestige = query.at(`/countries/${player}/prestige`);
    return { player, prestige };
  }
);

The alternative would be:

const buffer = readFileSync(args[0]);
const parser = await Jomini.initialize();
const save = parser.parseText(buffer, { encoding: "windows1252" });
const player = save.player;
const prestige = save.countries[player].prestige;

The faster version completes 40x faster (6.3s vs 0.16s) and uses about half the memory.

JSON

There is a middle ground in terms of performance and flexibility: using JSON as an intermediate layer:

const buffer = readFileSync(args[0]);
const parser = await Jomini.initialize();
const out = parser.parseText(buffer, { encoding: "windows1252" }, (q) => q.json());
const save = JSON.parse(out);
const player = save.player;
const prestige = save.countries[player].prestige;

The keys of the stringified JSON object are in the order as they appear in the file, so this makes the JSON approach well suited for parsing files where the order of object keys matter. The other APIs are subjected to natively constructed JS objects reordering keys to suit their fancy. To process the JSON and not lose key order, you'll want to leverage a streaming JSON parser like oboe.js or stream-json.

Interestingly, even though using JSON adds a layer, constructing and parsing the JSON into a JS object is still 3x faster than when a JS object is constructed directly. This must be a testament to how tuned browser JSON parsers are.

The JSON format does not change how dates are encoded, so dates are written into the JSON exactly as they appear in the original file.

The JSON generator contains options to tweak the output.

To pretty print the output:

parser.parseText(buffer, { }, (q) => q.json({ pretty: true }));

There is an option to decide how duplicate keys are serialized. For instance, given the following data:

core="AAA"
core="BBB"

The default behavior will group the two fields into one list, as shown below. This favors ergonomics, as the builtin JSON.parse doesn't handle duplicate keys well.

{
  "core": ["AAA", "BBB"]
}

If this behavior is not desirable, it can be tweaked such that the duplicate keys are preserved:

parser.parseText(buffer, { }, (q) => q.json({ duplicateKeyMode: "preserve" }));

will output:

{
  "core": "AAA",
  "core": "BBB"
}

Whether or not the above is valid JSON is debateable.

The remaining mode transforms key value objects to an array of key value tuples:

parser.parseText(buffer, { }, (q) => q.json({ duplicateKeyMode: "key-value-pairs" }));

will output:

{
  "type": "obj",
  "val": [
    ["core", "AAA"],
    ["core", "BBB"]
  ]
}

The output is ugly and verbose, but it's valid JSON and preserves the original structure. Arrays will have the type of array.

Data Mangling

The PDS data format is ambiguous without additional context in certain situations. A great example of this are EU4 armies. If a country only has a single army, then the parser will assume that army is singular object instead of an array. This can also been seen with individual units nested in an army. Below is an example of two armies, one army has a single unit while the other has multiple.

army={
  name="1st army"
  unit={ name="1st unit" }
}
army={
  name="2nd army"
  unit={ name="2nd unit" }
  unit={ name="3rd unit" }
}

Without intervention the parsed structure will be:

out = {
  army: [
    {
      name: "1st army",
      unit: { name: "1st unit", },
    },
    {
      name: "2nd army",
      unit: [
        { name: "2nd unit", },
        { name: "3rd unit", },
      ],
    },
  ],
}

// `army[0].unit` is an object
// `army[1].unit` is an array! 

This is remedied by passing the parsed struct through toArray and targeting the army.unit property

toArray(obj, "army.unit");
const expected = {
  army: [
    {
      name: "1st army",
      unit: [{ name: "1st unit", }, ],
    },
    {
      name: "2nd army",
      unit: [
        { name: "2nd unit", },
        { name: "3rd unit", },
      ],
    },
  ],
};

Write API

The write API is low level in order to clear out any ambiguities that may arise from a higher level API.

const jomini = await Jomini.initialize();
const out = jomini.write((writer) => {
  writer.write_unquoted("data");
  writer.write_object_start();
  writer.write_unquoted("settings");
  writer.write_array_start();
  writer.write_integer(0);
  writer.write_integer(1);
  writer.write_end();
  writer.write_unquoted("name");
  writer.write_quoted("world");
  writer.write_end();
  writer.write_unquoted("color");
  writer.write_header("rgb");
  writer.write_array_start();
  writer.write_integer(100);
  writer.write_integer(150);
  writer.write_integer(74);
  writer.write_end();
  writer.write_unquoted("start");
  writer.write_date(new Date(Date.UTC(1444, 10, 11)));
});

The return value will be a byte array that contains the following:

data={
  settings={
    0 1
  }
  name="world"
}
color=rgb {
  100 150 74
}
start=1444.11.11

There is not yet an official high level API to write out arbitrary objects; however, one can adapt this solution until a high level API is decided to be implemented.

Slim Module

By default, the jomini entrypoint includes Wasm that is base64 inlined. This is the default as most developers will probably not need to care. However some developers will care: those running the library in environments where Wasm is executable but not compilable or those who are ambitious about reducing compute and bandwidth costs for their users.

To cater to these use cases, there is a jomini/slim package that operates the exactly the same except now it is expected for developers to prime initialization through some other means:

import { Jomini } from "jomini/slim";
import wasm from "jomini/jomini.wasm";

const data = `player="FRA"`;
const parser = await Jomini.initialize({ wasm });
const out = parser.parseText(data);

Deno

Deno is currently supported through their npm specifier. Jomini requires --allow-read permissions.

import { Jomini } from "npm:[email protected]";

const data = await Deno.readAll(Deno.stdin);
const parser = await Jomini.initialize();
const out = parser.parseText(
  data,
  { encoding: "windows1252" },
  (query) => query.json(),
);

console.log(out);

jomini's People

Contributors

dependabot[bot] avatar jakubjafra avatar khartir avatar nickbabcock avatar rubybrowncoat avatar soryy708 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

jomini's Issues

Command line version?

I think a command line way of using this library would be an excellent extension.

Can not run tests on Windows

I'm using Windows 10 Home.
Node version v12.13.0 (LTS), npm version 6.12.0

I cloned the repository, ran npm install, and when I run npm run test I get the following output:

C:\Users\soryy\Documents\Software Engineering\Personal\jomini>npm run test

> [email protected] pretest C:\Users\soryy\Documents\Software Engineering\Personal\jomini
> npm run build


> [email protected] build C:\Users\soryy\Documents\Software Engineering\Personal\jomini
> jison -m commonjs -o ./lib/jomini.js ./lib/jomini.jison

> [email protected] test C:\Users\soryy\Documents\Software Engineering\Personal\jomini
> ./node_modules/mocha/bin/mocha

'.' is not recognized as an internal or external command,
operable program or batch file.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] test: `./node_modules/mocha/bin/mocha`
npm ERR! Exit status 1

Convert object to data file

Jomini converts data files generated by the Clausewitz engine into an object, but can not converts JS-object into Clausewitz engine data file. Why not do the method for reverse conversion? This would facilitate the creation of various editors.

How can I make this work with typescript and webpack

Hi

Assuming it's possible after reading your blog post here

After tweaking my configs as you pointed there, I can build it without an error now, but it says "fetch is not defined" when i try to initialize it in an electron app.

tsconfig.json

  "compilerOptions": {
    "outDir": "./build/",
    "noImplicitAny": true,
    "sourceMap": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "baseUrl": ".",
    "paths": {
      "~*": ["src*"],
    },
    "moduleResolution": "node",
    "strictNullChecks": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  }
}

webpack.config.js

{
  name: "main",
  entry: "./src/main.ts",
  target: "electron-main",
  output: {
    path: path.resolve(__dirname, "build"),
    filename: "main.js",
  },
  plugins: [],
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        loader: "ts-loader",
      },
      {
        enforce: "pre",
        test: /\.js$/,
        loader: "source-map-loader",
      },
      {
        test: /\.wasm/,
        type: 'asset/resource'
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".tsx"],
  },
}

Can you tell me what am i doing wrong?

Bug? Parsing map/area.txt

While trying to parse map/area.txt file (using json layer) I noticed odd results if the area has color attribute.

Sample of map/area.txt

brittany_area = { #5
	color = { 118  99  151 }

	169 170 171 172 4384
}

normandy_area = { #4
	167 168 1879 4385
}

The results received:

{
      "brittany_area": { "color": [118, 99, 151], "169": 170, "171": 172, "4384": null },
      "normandy_area": [167, 168, 1879, 4385]
}

normandy_area seems OK. brittany_area is off. The code is interpreting the odd ids as object properties.

Not sure what the correct format should be since "brittany_area": { "color": [118, 99, 151], [169, 170, 171, 172, 4384] } would be invalid. You would have to add your own property name.

Is it possible currently to tell Jomini to ignore pieces before it parses?

Thanks

Empty objects not handled

Empty objects seen in savefiles like
2626653={ }
throw Expecting 'IDENTIFIER', '=', 'QIDENTIFIER', 'NUMBER', 'DATE', '{', 'BOOL', 'hsv', 'rgb', 'QDATE', got '}'

Binary format

Do you plan to maybe add bindings for parsing binary files? Autosaves for some (all?) games are forced into binary format and it'd be very convenient to not have to manually use the rust lib.

Commit package-lock.json or disable it

Documentation about package-lock.json

package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of intermediate dependency updates.
This file is intended to be committed into source repositories, and serves various purposes:

  • Describe a single representation of a dependency tree such that teammates, deployments, and continuous integration are guaranteed to install exactly the same dependencies.
  • Provide a facility for users to “time-travel” to previous states of node_modules without having to commit the directory itself.
  • To facilitate greater visibility of tree changes through readable source control diffs.
  • And optimize the installation process by allowing npm to skip repeated metadata resolutions for previously-installed packages.

Either generate and commit package-lock.json (generated automatically when running npm install), or disable it entirely (by setting package-lock=false in a .npmrc file)

Out of memory error when parsing large files

Currently, I am getting FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory when the output of a large savegame file is passed into jomini.parse()
The save file I'm using is about 4.3 million lines 😝

Implementation is up to you, but maybe there is a way to chunk the parsing operation?

Do not package unnecessary files

Disclaimer: I do not use the package, this was found during automated npm packages downloading.

Your current package size is 9.0 MiB packed, 172 MiB unpacked.

Listing:

36M     ../tarballs.2.ex/jomini-0.2.0.tgz/autosave.eu4
4,3M    ../tarballs.2.ex/jomini-0.2.0.tgz/autosave.eu4.tmp
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/cli.js
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/gulpfile.js
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/index.js
24K     ../tarballs.2.ex/jomini-0.2.0.tgz/lex.js
24K     ../tarballs.2.ex/jomini-0.2.0.tgz/lib
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/LICENSE
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/package.json
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/README.md
272K    ../tarballs.2.ex/jomini-0.2.0.tgz/temp.json
28K     ../tarballs.2.ex/jomini-0.2.0.tgz/test
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/tmp
56K     ../tarballs.2.ex/jomini-0.2.0.tgz/tmp.json
132M    ../tarballs.2.ex/jomini-0.2.0.tgz/tmp.pretty.json
4,0K    ../tarballs.2.ex/jomini-0.2.0.tgz/tttt

I suspect that most of those, especially autosave.eu4, autosave.eu4.tmp, and tmp.pretty.json got packed to the release by mistake.

If you don't want to pack those files, add them to the .gitignore.

Is this repo able to return timeline data?

Title. I haven't found an issue with the project itself, I'm just inquiring to see if I could use this to pull each day within an EU4 timeline as JSON data. Would be useful for a project I'd like to work on.

Add support for escaped quotes

In Stellaris there are texts that contain double quotes ". These are escaped: \"
Example:

desc="A log of sorts was recovered from what is believed to have served as the spacecraft's bridge:

^QH\"Captain's chronicle. Begin recording.

Made forced landing on planet 55-23-X. Incompetent crew to blame. Shadba, Vuxal, and Dibrak the Third did not survive. Pirvax lost two secondary appendages and eleven of his eyes - remains in critical condition, survival doubtful. Rest are fine. Awaiting rescue, but surrounded by idiots. Very depressed.\"^Q!"

I'm unsure, if the control-characters are always there and the parser fails even without them.

This is an example for a failing test:

it('should handle escaped double quotes', function () {
  expect(parse("desc=\"\\\"Captain\\\"\"")).to.deep.equal({ 'desc': "\\\"Captain\\\"" });
});

Missing main file?

module.js:440
    throw err;
    ^

Error: Cannot find module './lib/jomini'
    at Function.Module._resolveFilename (module.js:438:15)
    at Function.Module._load (module.js:386:25)
    at Module.require (module.js:466:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (C:\Users\Arvin\AppData\Roaming\npm\node_modules\jomini\index.js:2:17)
    at Module._compile (module.js:541:32)
    at Object.Module._extensions..js (module.js:550:10)
    at Module.load (module.js:456:32)
    at tryModuleLoad (module.js:415:12)
    at Function.Module._load (module.js:407:3)

And yeah, in the lib folder there's no "jomini.js" file, as one would expect.

Not all stellaris files parse successfully

Seems like when you updated for stellaris you may have missed a couple new features of the script:

  • Variables - these are preceded with an @ symbol and then referenced throughout the file. Paradox pretty much uses them as file constants, so I am solving this currently by doing a preprocess step to enclose them in quotes, then a post process step to actually go through and do replacement.
  • Colors - these come in the format 'some_var = hsv { 0.0 0.5 1.0 }'. Again i'm solving this with a preprocessing step that wraps them in quotes so they are treated as strings for now.

You can find examples of both of these in /common/planet_classes/00_planet_classes.txt.

Thanks for your great work, this library has been a huge time saver for me!

Manual fixes required for EU4 gamestate data before parsing

Hello,

I'm playing an EU4 (v1.30.3) multiplayer game on a weekly basis and parsing the save game to calculate session scores based on some rules we've set.

Essentially once I've unzipped the gamestate file from the save and read it in as a string I go to parse it but encounter some errors. I've manually fixed these on the string before parsing and it now works fine but wondering if I can contribute any examples or help to get these fixed? I'm unsure if they're corruptions that have crept into the file or just weird syntactic edge cases.

I've attached the save mp_Dithmarschen1500_09_10.zip and here's the bodgy code I've put together to fix the string:

  const fixedGamestateString = gamestateString
    .split(`map_area_data{`)
    .join(`map_area_data={`)
    .split(`
{
\t\t\t}`)
    .join(``)
    .split(`
{STK\t\t\t}`).
    join(``)
    .split(`
{AKT\t\t\t}`)
    .join(``)

File Isn't Parsed Correctly

I'm attempting to parse EU4 event files using Jomini but somewhere along the line something is going wrong.

With RandomEvents.txt, for example, only data after the first province_event is parsed. After removing all instances of province_event, it seems only objects after the namespace declaration are parsed.

Is this a bug or am I doing something wrong?

(P.S. thanks so much for this project, it's amazing! Last time I parsed these files (without Jomini) it was a complete headache)

Getting geojson data of the current EU4 map?

Title again. Do you know somewhere where I could get geojson (or something similar) of the current EU4 map? Even better, is there a way to derive it from the game files? I thank you again for your help.

Incorrect types for slim package export

Importing from jomini/slim results in type errors or resolves to the any type (it's not clear why there's a different in resolved type).

Updating module and moduleResolution to Node16, and updating imports to include js file extension seems to fix the problem by having jomini create type declaration files that also have imports with file extensions. But this seems highly specific to esm, and I still want to support commonjs and other environments that don't use Node16 module resolution.

And I tried something like:

"typesVersions": {
  "*": {
    "slim": [
      "./dist/cjs-slim/index-slim.d.ts"
    ]
  }
},

But that didn't solve the issue and I don't know why 🤷

The temporary solution is to create a declaration file like custom.d.ts with the following in downstream projects:

declare module "jomini/slim" {
  export * from "jomini";
}

Strings containing numbers become numbers

This part of a Stellaris custom empire file:

		name={
			full_names={
				key="01"
			}
		}

Is parsed as:

      "name": {
        "full_names": {
          "key": 1
        }
      },

This is problematic, because it makes it hard to round-trip convert the JSON back to text.
Especially because the leading 0 is dropped.

I propose parsing quoted numbers as strings and not numbers.

Set up linter rules

ESLint is a JavaScript linter.
A linter is a white-box testing utility which runs static analysis on your code, and points out potential issues with style, code smells, and potential bugs.

ESLint is configured via .eslintrc file. I can set one up, but code standards & style guides need to be established first.

Can not handle > and <

some stellaris files in current version 2.3.1(such as "common/technology/00_synthetic_dawn_tech.txt") contains '>' , '<' , '>=' and '<=' operators will cause parse error.

research_leader = {
area = engineering
has_trait = "leader_trait_expertise_industry"
has_level > 2
}

It is a conditional statements that maybe can convert to 'GREATER_THAN'"SMALLER_THAN" in json.

"research_leader": {
"area": "engineering",
"has_trait": "leader_trait_expertise_industry",
"GREATER_THAN": {
"has_level" : 2
}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.