Add package-creator tool for making package files from JSON data

Rewritten from the PackageCreator Python script to use Principia's own package writing utilities instead
This commit is contained in:
ROllerozxa 2026-06-02 16:14:56 +02:00
commit 10e05d687b
7 changed files with 167 additions and 42 deletions

View file

@ -717,37 +717,41 @@ bool pkginfo::save() {
char *storage = (char*)pkgman::get_pkg_path(this->type);
snprintf(path, 1023, "%s/%d.ppkg", storage, this->id);
return save_to_path(path);
}
bool pkginfo::save_to_path(const char *path) {
FILE *fp = fopen(path, "wb");
if (fp) {
// always update the version to the latest on save
this->version = PKG_VERSION;
fwrite(&this->version, 1, 1, fp);
fwrite(&this->community_id, 1, sizeof(uint32_t), fp);
fwrite(this->name, 1, 255, fp);
fwrite(&this->unlock_count, 1, sizeof(uint8_t), fp);
fwrite(&this->first_is_menu, 1, sizeof(uint8_t), fp);
fwrite(&this->return_on_finish, 1, sizeof(uint8_t), fp);
fwrite(&this->num_levels, 1, sizeof(uint8_t), fp);
for (int x=0; x<this->num_levels; x++) {
fwrite(&this->levels[x], 1, sizeof(uint32_t), fp);
}
fclose(fp);
} else {
if (!fp) {
tms_errorf("could not open: %s", path);
return false;
}
// always update the version to the latest on save
this->version = PKG_VERSION;
fwrite(&this->version, 1, 1, fp);
fwrite(&this->community_id, 1, sizeof(uint32_t), fp);
fwrite(this->name, 1, 255, fp);
fwrite(&this->unlock_count, 1, sizeof(uint8_t), fp);
fwrite(&this->first_is_menu, 1, sizeof(uint8_t), fp);
fwrite(&this->return_on_finish, 1, sizeof(uint8_t), fp);
fwrite(&this->num_levels, 1, sizeof(uint8_t), fp);
for (int x=0; x<this->num_levels; x++) {
fwrite(&this->levels[x], 1, sizeof(uint32_t), fp);
}
fclose(fp);
return true;
}
bool pkginfo::open(int type, uint32_t id) {
if (type < 0 || type >= 4) {
tms_errorf("invalid level type");
return "";
return false;
}
char path[1024];
@ -767,37 +771,38 @@ bool pkginfo::open(int type, uint32_t id) {
FILE *fp = _fopen(path, "rb");
if (fp) {
_fread(&this->version, 1, 1, fp);
if (this->version >= 2)
_fread(&this->community_id, 1, sizeof(uint32_t), fp);
else
this->community_id = 0;
_fread(this->name, 1, 255, fp);
if (this->version >= 3)
_fread(&this->unlock_count, 1, sizeof(uint8_t), fp);
if (this->version >= 1) {
_fread(&this->first_is_menu, 1, sizeof(uint8_t), fp);
_fread(&this->return_on_finish, 1, sizeof(uint8_t), fp);
}
_fread(&this->num_levels, 1, sizeof(uint8_t), fp);
if (this->num_levels)
this->levels = (uint32_t*)malloc(this->num_levels * sizeof(uint32_t));
for (int x=0; x<this->num_levels; x++) {
_fread(&this->levels[x], 1, sizeof(uint32_t), fp);
}
_fclose(fp);
} else {
if (!fp) {
tms_errorf("could not open: %s", path);
return false;
}
_fread(&this->version, 1, 1, fp);
if (this->version >= 2)
_fread(&this->community_id, 1, sizeof(uint32_t), fp);
else
this->community_id = 0;
_fread(this->name, 1, 255, fp);
if (this->version >= 3)
_fread(&this->unlock_count, 1, sizeof(uint8_t), fp);
if (this->version >= 1) {
_fread(&this->first_is_menu, 1, sizeof(uint8_t), fp);
_fread(&this->return_on_finish, 1, sizeof(uint8_t), fp);
}
_fread(&this->num_levels, 1, sizeof(uint8_t), fp);
if (this->num_levels)
this->levels = (uint32_t*)malloc(this->num_levels * sizeof(uint32_t));
for (int x=0; x<this->num_levels; x++) {
_fread(&this->levels[x], 1, sizeof(uint32_t), fp);
}
_fclose(fp);
return true;
}

View file

@ -180,6 +180,7 @@ class pkgman {
}
};
/// Class representing a package of levels (stored as .ppkg files)
class pkginfo {
public:
uint8_t version;
@ -292,8 +293,14 @@ class pkginfo {
return true;
}
/// Open package by type and ID.
bool open(int type, uint32_t id);
/// Save current package as .ppkg determined by type and ID.
bool save();
/// Save package to a specific path, rather than the default path determined by type and ID.
bool save_to_path(const char *path);
};
// level flags

View file

@ -1,4 +1,4 @@
SUBDIRS := featured-list-creator lvl-icon-extractor lvlbuf-decompressor lvledit progress-get
SUBDIRS := featured-list-creator lvl-icon-extractor lvlbuf-decompressor lvledit package-creator progress-get
.PHONY: all clean $(SUBDIRS)

View file

@ -13,4 +13,5 @@ Most of these programs rely on source files from the main Principia codebase and
- `lvl-icon-extractor`: Extract the embedded level icon in a Principia level file
- `lvlbuf-decompressor`: Decompress the level buffer of a Principia level file
- `lvledit`: Edit metadata of Principia level files
- `package-creator`: Create a Principia package file from JSON data
- `progress-get`: Get leaderboard score for a given level from a data.bin file

View file

@ -0,0 +1,28 @@
BIN = ../bin
PRINCIPIA = ../..
PROGRAM = package-creator
SRCS := main.cc $(PRINCIPIA)/src/pkgman.cc $(PRINCIPIA)/src/rand.c
# always static unless you do STATIC=0
STATIC ?= 1
CXX ?= g++
CXXFLAGS ?= -D_NO_TMS -O2 -ffunction-sections -fdata-sections
INCLUDES ?= -I$(PRINCIPIA)/src -I$(PRINCIPIA)/lib
LDFLAGS ?= -lz -Wl,--gc-sections
ifeq ($(STATIC),1)
LDFLAGS += -l:libjansson.a
else
LDFLAGS += -ljansson
endif
all: $(BIN)/$(PROGRAM)
$(BIN)/$(PROGRAM): $(SRCS)
@mkdir -p $(BIN)
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ $^ $(LDFLAGS)
clean:
rm $(BIN)/$(PROGRAM)
.PHONY: all clean

View file

@ -0,0 +1,27 @@
# `package-creator`
Create a Principia package file from JSON data.
For more information about Principia level packages, see the [wiki](https://principia-web.se/wiki/Packages).
## Usage
```bash
package-creator <package-json> <package-file>
```
## Format
Anything except for `name` and `levels` is optional, the default values are the ones provided below:
```json
{
"name": "Test Package",
"community_id": 0,
"levels_unlocked": 2,
"first_is_menu": 0,
"return_on_finish": 0,
"levels": [
67,
68
]
}
```

View file

@ -0,0 +1,57 @@
#include "pkgman.hh"
#include <jansson.h>
#include <stdio.h>
#define GET_JSON_OBJ(obj, key) json_object_get((obj), (key))
#define GET_JSON_INTEGER(obj, key, def) json_integer_value(GET_JSON_OBJ((obj), (key)) ? GET_JSON_OBJ((obj), (key)) : json_integer((def)))
#define GET_JSON_STRING(obj, key) json_string_value(GET_JSON_OBJ((obj), (key)))
int main(int argc, char **argv) {
if (argc < 3) {
printf("usage: package-creator [package-json] [package-file]\n");
return 1;
}
const char *input_filename = argv[1];
const char *output_filename = argv[2];
json_t *root; json_error_t error;
root = json_load_file(input_filename, 0, &error);
if (!root) {
printf("Error loading JSON file: %s\n", error.text);
return 1;
}
json_t *levels = json_object_get(root, "levels");
// Create pkginfo object from JSON data
pkginfo p;
p.version = 3;
p.community_id = GET_JSON_INTEGER(root, "community_id", 0);
p.unlock_count = GET_JSON_INTEGER(root, "unlock_count", 2);
p.first_is_menu = GET_JSON_INTEGER(root, "first_is_menu", 0);
p.return_on_finish = GET_JSON_INTEGER(root, "return_on_finish", 0);
strncpy(p.name, GET_JSON_STRING(root, "name"), 255);
p.name[255] = '\0';
int num_levels = json_array_size(levels);
if (num_levels > 255) {
printf("Too many levels in package (max 255)\n");
return 1;
}
p.num_levels = num_levels;
if (p.num_levels > 0)
p.levels = (uint32_t*)malloc(p.num_levels * sizeof(uint32_t));
for (size_t i = 0; i < p.num_levels; i++)
p.levels[i] = (uint32_t)json_integer_value(json_array_get(levels, i));
// Write the package to a file
p.save_to_path(output_filename);
return 0;
}