liblcf
liblcf copied to clipboard
Add enums for event command parameters
Currently Player uses dozens of magic numbers for event command parameters. For more readable code, mostly in game interpreter and shared and better support for game editors, liblcf might provide some generated code from CSV with these enumerations.
Number of hits of com.parameters
in code:
-
game_interpreter.cpp
: 529 -
game_interpreter_map.cpp
: 52 -
game_interpreter_battle.cpp
: 42
I did some research on this:
Ghabry Suggested checking R48's commands descriptions, from @20kdc repos:
https://github.com/20kdc/gabien-app-r48/blob/master/app/src/main/resources/assets/R2K/CommandsGI_0.txt
There are also some interesting references from Destiny Patch sourcecode (thanks to Kotatsu Akira and @drxgb) Object_Command.zip
The Destiny Patch one also has lots of name suggestions for some parameters outputs, maybe making things friendlier for the creation of a scripting language.
It may complicate stuff in cases where maniacs patch is used, due to its alien bitsmask parameters... The code from it is a WIP assembly file, that Kotatsu made for her Destiny Script update. It has some of those maniacs patch specifications, but it may be oudated due to how mp chances every week.
just so you know, some commands are... complicated, and can't be directly dumped from r48's CMDB due to this that said if you need a JSONification of what's available I can rig up something if asked
just so you know, some commands are... complicated, and can't be directly dumped from r48's CMDB due to this that said if you need a JSONification of what's available I can rig up something if asked
Hey @20kdc! It would help us a lot, since we are stuck with this issue since 2018
Meanwhile I extracted all the commented const
from the Destiny Script asm file:
commonParameters.csv
This isn't absolutely all data in the system (it gets increasingly complicated to do that going further into the details) but it should have an entry for every command. cmdb.json.zip
Oh, right, I should probably note:
- There are two command databases,
event
andmove
. (Move commands are considered just another type of command in R48's system.) - All RPG Maker 2000/2003 commands are considered as having a string as parameter 0.
-
specialSchemaEssential
is a flag indicating that the parameters given aren't necessarily accurate and basically you can chuck them out a window, because the command does something really specific that can't be expressed with a simple array-of-parameters format. -
nameRawUnlocalized
is written using S-expressions and custom stuff. Details are in the R48 development documentation, but I wouldn't really recommend trying to parse these unless you have to. They're basically inline stringifier logic. -
name
is directly derived fromnameRawUnlocalized
, i.e. you can derivename
fromnameRawUnlocalized
given the prerequisite support code. (There is no C++ version of said support code, but a good start would be to have a Scheme implementation with Scheme 9 From Empty Space's macro semantics.) - Schema element indices are represented as integers. To map these to names, create a reverse lookup table for
sdbID
. The main purpose of including an SDB dump was to allow access to enum values, but I've included some other stuff just in case (it should just about get you far enough to read special schemas). - Dynamic schema elements aren't included. Not that there is a reason to do so; it would just dump the contents of TestGame-2000's database into the file.
Thanks!
seems to be using the default vanilla order and looks easy to navigate by cycling through
JSON.cmdbs.event.knownCommands[n].params[m].name
// e.g.: "positionLocked" or "null" if unused string
and in order to match with our IDs, it can be get through:
JSON.cmdbs.event.knownCommands[n].commandId
// e.g.: 10120
I thought about replacing all se
with sdbID key names
but I wonder if it is useful for us when building a .csv file...
This is what I made:
CommandParameters.csv
The js do get it was:
// Determine the maximum number of parameters among all commands
let maxParams = 0;
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
if (command.params.length > maxParams) {
maxParams = command.params.length;
}
});
// Create CSV header
const headers = ['ParentName', 'ParentId'];
for (let i = 0; i < maxParams; i++) {
headers.push(`Param_${i}`);
}
// Create CSV rows
const csvData = [headers];
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
const row = [command.name, command.commandId];
for (let i = 0; i < maxParams; i++) {
if (i < command.params.length) {
row.push(command.params[i].name);
} else {
row.push(''); // Empty cell if no parameter at this index
}
}
csvData.push(row);
});
// Convert the data to CSV format
const csvContent = csvData.map((row) => row.join(',')).join('\n');
console.log('CSV data as a string:');
console.log(csvContent);
That script isn't handling parameters with dynamic types (i.e. 10310 has goldIsVar
and after that, either the literal value of gold or the variable ID)
hm... right. Though that kind of detection is already inside the Player's source code. It should indeed have inside it something like goldValue
or whatever...
if you look at the JSON, you can get at least a first pass just by taking the default branch of "dynamic" parameters
EDIT: Example:
{
"arrayDI" : 2,
"contents" : {
"0" : {
"name" : "gold",
"se" : 285,
"type" : "static"
}
},
"def" : {
"name" : "goldVar",
"se" : 50,
"type" : "static"
},
"type" : "dynamic"
}
...oh, right, also, something I forgot to mention: The sdbNodes stuff was included for a reason! you can use it to find and retrieve enums
Update:
// Determine the maximum number of parameters among all commands
let maxParams = 0;
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
if (command.params.length > maxParams) {
maxParams = command.params.length;
}
});
// Create CSV header
const headers = ['ParentName', 'ParentId'];
for (let i = 0; i < maxParams; i++) {
headers.push(`Param_${i}`);
}
// Create CSV rows
const csvData = [headers];
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
const row = [command.name, command.commandId];
for (let i = 0; i < maxParams; i++) {
if (i < command.params.length) {
const param = command.params[i];
if (param.type === 'dynamic' && param.contents && param.contents[0]) {
row.push(param.contents[0].name);
} else {
row.push(param.name);
}
} else {
row.push(''); // Empty cell if no parameter at this index
}
}
csvData.push(row);
});
// Convert the data to CSV format
const csvContent = csvData.map((row) => row.join(',')).join('\n');
console.log('CSV data as a string:');
console.log(csvContent);
I'd still advise checking the "def"
field, as while for booleans things are written with it being "true", in other cases it's either a default or a fallback for unrecognized values
You mean, checking if def exists instead of param.type === 'dynamic' ?
I gotta leave right now, later I'll take a better look at still needs to be done and what sdbNodes does.
You mean, checking if def exists instead of param.type === 'dynamic' ?
I gotta leave right now, later I'll take a better look at still needs to be done and what sdbNodes does.
Well, for when you get back, I'll just explain the fine details:
Param is an abstract class in R48, with two implementations: Static and Dynamic. These are represented as the "static" and "dynamic" types.
If param.type == "dynamic"
, the following will be true:
- "def" exists, and is the default param
- "contents" exists, and contains alternate param values
- "arrayDI" indicates which parameter is used to control this parameter
For static params, it's "name" (name) and "se" (index into "sdbNodes") as usual.
"sdbNodes" is an array because a lot of SDB nodes are just completely unnamed, and some have two names, etc. That said, a name will only ever refer to one SDB node. You can find the mapping in the "sdbID" object.
The SDB data in the JSON is... incomplete. But it has things like enumerations, so you can use this to get, say, window position enumerations and such. The "se" value in parameters connects those enumerations to parameters.
The "specialSchema" value is for commands which don't fit the Param system; depending on how much or little effort you want to put into really squeezing every last bit of data out of the JSON, you may be better off just ignoring this.
Now it has the same command Names from easyRPG sourcecode.
EventCommands.csv
I received some feedback about more entries:
@20kdc do you have any info on those?
The source code of the JS extractor (included both jsonFile and BetterName templates, to show where goes r48 json and easyRPG commandNames)
// Replace this with your JSON data
const jsonFile = {
cmdbs: {
event: {
knownCommands: [
{
name: 'Command1',
commandId: 'ID1',
params: [
{ name: 'Param_0_Value', type: 'static' },
{ name: 'Param_1_Value', type: 'dynamic', contents: [{ name: 'Dynamic_Param_1' }] },
],
},
{
name: 'Command2',
commandId: 'ID2',
params: [
{ name: 'Param_0_Value', type: 'dynamic', contents: [{ name: 'Dynamic_Param_0' }] },
{ name: 'Param_1_Value', type: 'static' },
{ name: 'Param_2_Value', type: 'dynamic', contents: [{ name: 'Dynamic_Param_2' }] },
],
},
],
},
},
};
const betterNames = {
"ID1": "BetterCommand1",
"ID2": "BetterCommand2",
// Add more mappings as needed
};
// Determine the maximum number of parameters among all commands
let maxParams = 0;
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
if (command.params.length > maxParams) {
maxParams = command.params.length;
}
});
// Create CSV header
const headers = ['ParentName', 'ParentId'];
for (let i = 0; i < maxParams; i++) {
headers.push(`Param_${i}`);
}
// Create CSV rows
const csvData = [headers];
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
const row = [betterNames[command.commandId] || command.name, command.commandId];
for (let i = 0; i < maxParams; i++) {
if (i < command.params.length) {
const param = command.params[i];
if (param.type === 'dynamic' && param.contents && param.contents[0]) {
row.push(param.contents[0].name);
} else {
row.push(param.name);
}
} else {
row.push(''); // Empty cell if no parameter at this index
}
}
csvData.push(row);
});
// Convert the data to CSV format
const csvContent = csvData.map((row) => row.join(',')).join('\n');
console.log('CSV data as a string:');
console.log(csvContent);
The relation between the "value" and "type of value" parameter is, as I have previously explained, in the "dynamic" param structure. "arrayDI" is the index of the "type of value" parameter.
ok, put more information inside it. Remember that this only covers the R48 supported commands, it lacks the new commands and parameters ported from Maniacs Patch.
Let me know if something else is missing:
jsonFile = {} //R48 json file
easyrpgNames = {} //easyRPG list of commands names
// Determine the maximum number of parameters among all commands
let maxParams = 0;
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
if (command.params.length > maxParams) {
maxParams = command.params.length;
}
});
// Create CSV header
const headers = ['Command', 'ID']; // this instead of ParentName and ParentId
for (let i = 0; i < maxParams; i++) {
if (i === 0) {
headers.push('String'); // Rename Param_0 to String
} else {
headers.push(`Param_${i}`);
}
}
// Create CSV rows
const csvData = [headers];
jsonFile.cmdbs.event.knownCommands.forEach((command) => {
const row = [easyrpgNames[command.commandId] || command.name.replace(/[^a-zA-Z0-9_]/g, ''), command.commandId];
for (let i = 0; i < maxParams; i++) {
if (i < command.params.length) {
const param = command.params[i];
if (param.type === 'dynamic') {
// Construct the Param_n cell based on additional rules
let paramCell = param.def && param.def.name ? param.def.name : 'undefined'; // Default parameter name
if (param.contents) {
const contentsNames = Object.keys(param.contents).map((o) => param.contents[o].name);
if (contentsNames.length > 0) {
paramCell += ' || ' + contentsNames.join(' || '); // Contents names
}
}
if (param.arrayDI != null) {
paramCell += ` (ParentParam: ${param.arrayDI})`; // ParentName is used here now.
}
row.push(paramCell);
} else {
const paramName = param.name
row.push(paramName);
}
} else {
row.push(''); // Empty cell if no parameter at this index
}
}
csvData.push(row);
});
// Convert the data to CSV format
const csvContent = csvData.map((row) => row.join(',')).join('\n');
console.log('CSV data as a string:');
//console.log(csvContent);
csvContent;
cases like set variable and timer operation doesn't have parameters described on it.
those cases always have specialSchemaEssential: true
+ specialSchema: aNumberValue
on R48 Json.
example from timer operation:
specialSchemaEssential: true
specialSchema: 423`
that points towards this key and value inside sdbID
Yes; this is because it's hard or impossible to describe those cases using the CMDB syntax. Same goes for Conditional Branch. In fact, there's probably argument to be made against converting them period, at least at first; they're mazes and mistakes will result in awkward Player bugs.
ok, just to keep things fresh:
List of Event Commands: https://github.com/EasyRPG/liblcf/files/12564301/EventCommands.4.csv
List of named Parameters: https://github.com/EasyRPG/liblcf/files/12515333/commonParameters.csv
@Ghabry let me know what else is missing in those files.
I extracted all the com.parameters[n]
and com.string
references from their .cpp files:
commandsnippets.zip
I'm trying to discover if there's a painless way to customize the csv list with new commands supported by easyRPG, using these snippets of code.
Another motivation for this is shown here:
test
is how it is currently done (approximately).
test2
is with a wrapper class that exposes getter function for all the individual stuff. The wrapper can be generated and then just pass the event command to the current wrapper and invoke the functions. This is more readable as the intention is more clear and the magic numbers are hidden.
When you look at the assembler output you can see that both functions compile to the exact same code. Compilers are smart :).