mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Created yaml file with opcode definitions (70% complete).
This commit is contained in:
parent
83d32dfe99
commit
fba6ac3e10
@ -116,8 +116,13 @@ Features that are in ***bold italics*** are planned and not yet implemented.
|
||||
|
||||
- ***Figure out how they work***
|
||||
|
||||
## Non-BlueBurst Support
|
||||
|
||||
- Support different sets of instructions (older versions had no stack)
|
||||
|
||||
## Bugs
|
||||
|
||||
- [Script Object Code](#script-object-code): Correctly deal with stack arguments (e.g. when a function expects a u32, pushing a u8, u16, u32 or register value is ok) (when a function expects a register reference, arg_pushb should be used)
|
||||
- [3D View](#3d-view): Random Type Box 1 and Fixed Type Box objects aren't rendered correctly
|
||||
- [3D View](#3d-view): Some objects are only partially loaded (they consist of several seperate models)
|
||||
- Forest Switch
|
||||
|
112
assets_generation/resources/scripting/opcodes.schema.json
Normal file
112
assets_generation/resources/scripting/opcodes.schema.json
Normal file
@ -0,0 +1,112 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Phantasy Star Online Instruction Set",
|
||||
"type": "object",
|
||||
"required": ["opcodes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"opcodes": {
|
||||
"type": "array",
|
||||
"description": "List of every known opcode.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/opcode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"opcode": {
|
||||
"type": "object",
|
||||
"required": ["code", "params"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 63999,
|
||||
"description": "Unique byte representation of the opcode."
|
||||
},
|
||||
"mnemonic": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9=<>!_]+$",
|
||||
"description": "Unique string representation of the opcode."
|
||||
},
|
||||
"doc": {
|
||||
"type": "string",
|
||||
"description": "Opcode documentation."
|
||||
},
|
||||
"params": {
|
||||
"type": "array",
|
||||
"description": "Opcode parameters. Whether or not the stack is used is determined by the stack property.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/param"
|
||||
}
|
||||
},
|
||||
"stack": {
|
||||
"enum": ["push", "pop"],
|
||||
"description": "Stack interaction. \"push\" if the instruction takes immediate arguments and pushes its arguments onto the stack. \"pop\" if the instruction doesn't take immediate arguments but pops its arguments off the stack."
|
||||
}
|
||||
}
|
||||
},
|
||||
"param": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"$ref": "#/definitions/param_type"
|
||||
},
|
||||
"doc": {
|
||||
"type": "string",
|
||||
"description": "Parameter-specific documentation."
|
||||
},
|
||||
"access": {
|
||||
"$ref": "#/definitions/access"
|
||||
},
|
||||
"reg_tup": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "Specifies the way the referenced registers will be interpreted. Should only be specified if the parameter type is \"reg_tup_ref\".",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["type", "access"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"$ref": "#/definitions/param_type"
|
||||
},
|
||||
"doc": {
|
||||
"type": "string"
|
||||
},
|
||||
"access": {
|
||||
"$ref": "#/definitions/access"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"param_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"any",
|
||||
"byte",
|
||||
"word",
|
||||
"dword",
|
||||
"float",
|
||||
"label",
|
||||
"i_label",
|
||||
"d_label",
|
||||
"string",
|
||||
"reg_ref",
|
||||
"reg_tup_ref",
|
||||
"reg_ref_var",
|
||||
"pointer"
|
||||
]
|
||||
},
|
||||
"access": {
|
||||
"type": "string",
|
||||
"enum": ["read", "write", "read_write"],
|
||||
"description": "Specifies the way the instruction accesses the referenced register(s)."
|
||||
}
|
||||
}
|
||||
}
|
3145
assets_generation/resources/scripting/opcodes.yml
Normal file
3145
assets_generation/resources/scripting/opcodes.yml
Normal file
File diff suppressed because it is too large
Load Diff
@ -624,7 +624,7 @@ function parse_instruction_arguments(cursor: Cursor, opcode: Opcode): Arg[] {
|
||||
case Type.F32:
|
||||
args.push({ value: cursor.f32(), size: 4 });
|
||||
break;
|
||||
case Type.Register:
|
||||
case Type.RegRef:
|
||||
args.push({ value: cursor.u8(), size: 1 });
|
||||
break;
|
||||
case Type.ILabel:
|
||||
@ -708,7 +708,7 @@ function write_object_code(
|
||||
case Type.F32:
|
||||
cursor.write_f32(arg.value);
|
||||
break;
|
||||
case Type.Register:
|
||||
case Type.RegRef:
|
||||
cursor.write_u8(arg.value);
|
||||
break;
|
||||
case Type.ILabel:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -388,7 +388,7 @@ class Assembler {
|
||||
|
||||
switch (param.type) {
|
||||
case Type.U8:
|
||||
case Type.Register:
|
||||
case Type.RegRef:
|
||||
this.add_instruction(Opcode.arg_pushb, [arg]);
|
||||
break;
|
||||
case Type.U16:
|
||||
@ -503,7 +503,7 @@ class Assembler {
|
||||
|
||||
break;
|
||||
case TokenType.Register:
|
||||
match = param.type === Type.Register;
|
||||
match = param.type === Type.RegRef;
|
||||
this.parse_register(token, args);
|
||||
break;
|
||||
case TokenType.String:
|
||||
@ -543,7 +543,7 @@ class Assembler {
|
||||
case Type.F32:
|
||||
type_str = "a float";
|
||||
break;
|
||||
case Type.Register:
|
||||
case Type.RegRef:
|
||||
type_str = "a register reference";
|
||||
break;
|
||||
case Type.ILabel:
|
||||
|
@ -37,27 +37,31 @@ export class BasicBlock {
|
||||
|
||||
export class ControlFlowGraph {
|
||||
readonly blocks: BasicBlock[] = [];
|
||||
readonly instructions: Map<Instruction, BasicBlock> = new Map();
|
||||
|
||||
private readonly instructions_to_block: Map<Instruction, BasicBlock> = new Map();
|
||||
private readonly labels_to_block = new Map<number, BasicBlock>();
|
||||
|
||||
get_block_for_instuction(instruction: Instruction): BasicBlock | undefined {
|
||||
return this.instructions_to_block.get(instruction);
|
||||
}
|
||||
|
||||
get_block_for_label(label: number): BasicBlock | undefined {
|
||||
return this.labels_to_block.get(label);
|
||||
}
|
||||
|
||||
static create(segments: InstructionSegment[]): ControlFlowGraph {
|
||||
const cfg = new ControlFlowGraph();
|
||||
// Mapping of labels to basic blocks.
|
||||
const label_blocks = new Map<number, BasicBlock>();
|
||||
|
||||
for (const segment of segments) {
|
||||
create_basic_blocks(cfg, label_blocks, segment);
|
||||
this.create_basic_blocks(cfg, segment);
|
||||
}
|
||||
|
||||
link_blocks(cfg, label_blocks);
|
||||
this.link_blocks(cfg);
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
|
||||
function create_basic_blocks(
|
||||
cfg: ControlFlowGraph,
|
||||
label_blocks: Map<number, BasicBlock>,
|
||||
segment: InstructionSegment
|
||||
) {
|
||||
private static create_basic_blocks(cfg: ControlFlowGraph, segment: InstructionSegment) {
|
||||
const len = segment.instructions.length;
|
||||
let start = 0;
|
||||
let first_block = true;
|
||||
@ -143,14 +147,14 @@ function create_basic_blocks(
|
||||
const block = new BasicBlock(segment, start, i + 1, branch_type, branch_labels);
|
||||
|
||||
for (let j = block.start; j < block.end; j++) {
|
||||
cfg.instructions.set(block.segment.instructions[j], block);
|
||||
cfg.instructions_to_block.set(block.segment.instructions[j], block);
|
||||
}
|
||||
|
||||
cfg.blocks.push(block);
|
||||
|
||||
if (first_block) {
|
||||
for (const label of segment.labels) {
|
||||
label_blocks.set(label, block);
|
||||
cfg.labels_to_block.set(label, block);
|
||||
}
|
||||
|
||||
first_block = false;
|
||||
@ -160,7 +164,7 @@ function create_basic_blocks(
|
||||
}
|
||||
}
|
||||
|
||||
function link_blocks(cfg: ControlFlowGraph, label_blocks: Map<number, BasicBlock>): void {
|
||||
private static link_blocks(cfg: ControlFlowGraph): void {
|
||||
// Pairs of calling block and block to which callees should return to.
|
||||
const callers: [BasicBlock, BasicBlock][] = [];
|
||||
|
||||
@ -185,7 +189,7 @@ function link_blocks(cfg: ControlFlowGraph, label_blocks: Map<number, BasicBlock
|
||||
}
|
||||
|
||||
for (const label of block.branch_labels) {
|
||||
const to_block = label_blocks.get(label);
|
||||
const to_block = cfg.labels_to_block.get(label);
|
||||
|
||||
if (to_block) {
|
||||
block.link_to(to_block);
|
||||
@ -194,7 +198,8 @@ function link_blocks(cfg: ControlFlowGraph, label_blocks: Map<number, BasicBlock
|
||||
}
|
||||
|
||||
for (const [caller, ret] of callers) {
|
||||
link_returning_blocks(label_blocks, ret, caller);
|
||||
link_returning_blocks(cfg.labels_to_block, ret, caller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,6 +69,9 @@ export class ValueSet {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't take into account interger overflow.
|
||||
*/
|
||||
scalar_add(s: number): ValueSet {
|
||||
for (const int of this.intervals) {
|
||||
int.start += s;
|
||||
@ -78,10 +81,16 @@ export class ValueSet {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't take into account interger overflow.
|
||||
*/
|
||||
scalar_sub(s: number): ValueSet {
|
||||
return this.scalar_add(-s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't take into account interger overflow.
|
||||
*/
|
||||
scalar_mul(s: number): ValueSet {
|
||||
for (const int of this.intervals) {
|
||||
int.start *= s;
|
||||
@ -93,6 +102,7 @@ export class ValueSet {
|
||||
|
||||
/**
|
||||
* Integer division.
|
||||
* Doesn't take into account interger overflow.
|
||||
*/
|
||||
scalar_div(s: number): ValueSet {
|
||||
for (const int of this.intervals) {
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { InstructionSegment, SegmentType, Opcode } from "../../data_formats/parsing/quest/bin";
|
||||
import { InstructionSegment, Opcode, SegmentType } from "../../data_formats/parsing/quest/bin";
|
||||
import { assemble } from "../assembly";
|
||||
import { ControlFlowGraph } from "./ControlFlowGraph";
|
||||
import { register_values } from "./register_values";
|
||||
import {
|
||||
MAX_REGISTER_VALUE,
|
||||
MIN_REGISTER_VALUE,
|
||||
register_values,
|
||||
REGISTER_VALUES,
|
||||
} from "./register_values";
|
||||
|
||||
test(`${register_values.name} trivial case`, () => {
|
||||
const im = to_instructions(`
|
||||
@ -46,6 +51,19 @@ test(`${register_values.name} two code paths`, () => {
|
||||
expect(values.get(1)).toBe(222);
|
||||
});
|
||||
|
||||
test(`${register_values.name} loop`, () => {
|
||||
const im = to_instructions(`
|
||||
0:
|
||||
addi r10, 5
|
||||
jmpi_< r10, 500, 0
|
||||
ret
|
||||
`);
|
||||
const cfg = ControlFlowGraph.create(im);
|
||||
const values = register_values(cfg, im[0].instructions[2], 10);
|
||||
|
||||
expect(values.size()).toBe(REGISTER_VALUES);
|
||||
});
|
||||
|
||||
test(`${register_values.name} leta and leto`, () => {
|
||||
const im = to_instructions(`
|
||||
0:
|
||||
@ -56,15 +74,15 @@ test(`${register_values.name} leta and leto`, () => {
|
||||
const cfg = ControlFlowGraph.create(im);
|
||||
const r0 = register_values(cfg, im[0].instructions[2], 0);
|
||||
|
||||
expect(r0.size()).toBe(Math.pow(2, 32));
|
||||
expect(r0.min()).toBe(-Math.pow(2, 31));
|
||||
expect(r0.max()).toBe(Math.pow(2, 31) - 1);
|
||||
expect(r0.size()).toBe(REGISTER_VALUES);
|
||||
expect(r0.min()).toBe(MIN_REGISTER_VALUE);
|
||||
expect(r0.max()).toBe(MAX_REGISTER_VALUE);
|
||||
|
||||
const r1 = register_values(cfg, im[0].instructions[2], 1);
|
||||
|
||||
expect(r1.size()).toBe(Math.pow(2, 32));
|
||||
expect(r1.min()).toBe(-Math.pow(2, 31));
|
||||
expect(r1.max()).toBe(Math.pow(2, 31) - 1);
|
||||
expect(r1.size()).toBe(REGISTER_VALUES);
|
||||
expect(r1.min()).toBe(MIN_REGISTER_VALUE);
|
||||
expect(r1.max()).toBe(MAX_REGISTER_VALUE);
|
||||
});
|
||||
|
||||
test(`${register_values.name} rev`, () => {
|
||||
@ -147,8 +165,8 @@ test(`${register_values.name} get_random`, () => {
|
||||
|
||||
const v1 = register_values(cfg, im[0].instructions[5], 10);
|
||||
|
||||
expect(v0.size()).toBe(1);
|
||||
expect(v0.get(0)).toBe(20);
|
||||
expect(v1.size()).toBe(1);
|
||||
expect(v1.get(0)).toBe(20);
|
||||
|
||||
const v2 = register_values(cfg, im[0].instructions[7], 10);
|
||||
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { Instruction, Opcode } from "../../data_formats/parsing/quest/bin";
|
||||
import {
|
||||
Instruction,
|
||||
Opcode,
|
||||
RegTupRefType,
|
||||
TYPE_REG_REF,
|
||||
TYPE_REG_REF_VAR,
|
||||
} from "../../data_formats/parsing/quest/bin";
|
||||
import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
|
||||
import { ValueSet } from "./ValueSet";
|
||||
|
||||
const MIN_REGISTER_VALUE = -Math.pow(2, 31);
|
||||
const MAX_REGISTER_VALUE = Math.pow(2, 31) - 1;
|
||||
export const MIN_REGISTER_VALUE = -Math.pow(2, 31);
|
||||
export const MAX_REGISTER_VALUE = Math.pow(2, 31) - 1;
|
||||
export const REGISTER_VALUES = Math.pow(2, 32);
|
||||
|
||||
/**
|
||||
* Computes the possible values of a register at a specific instruction.
|
||||
@ -13,7 +20,7 @@ export function register_values(
|
||||
instruction: Instruction,
|
||||
register: number
|
||||
): ValueSet {
|
||||
const block = cfg.instructions.get(instruction);
|
||||
const block = cfg.get_block_for_instuction(instruction);
|
||||
|
||||
if (block) {
|
||||
let inst_idx = block.start;
|
||||
@ -26,13 +33,18 @@ export function register_values(
|
||||
inst_idx++;
|
||||
}
|
||||
|
||||
return find_values(block, inst_idx, register);
|
||||
return find_values(new Set(), block, inst_idx, register);
|
||||
} else {
|
||||
return new ValueSet();
|
||||
}
|
||||
}
|
||||
|
||||
function find_values(block: BasicBlock, end: number, register: number): ValueSet {
|
||||
function find_values(
|
||||
path: Set<BasicBlock>,
|
||||
block: BasicBlock,
|
||||
end: number,
|
||||
register: number
|
||||
): ValueSet {
|
||||
let values = new ValueSet();
|
||||
|
||||
for (let i = block.start; i < end; i++) {
|
||||
@ -42,22 +54,17 @@ function find_values(block: BasicBlock, end: number, register: number): ValueSet
|
||||
switch (instruction.opcode) {
|
||||
case Opcode.let:
|
||||
if (args[0].value === register) {
|
||||
values = find_values(block, i, args[1].value);
|
||||
values = find_values(new Set(path), block, i, args[1].value);
|
||||
}
|
||||
break;
|
||||
case Opcode.leti:
|
||||
case Opcode.letb:
|
||||
case Opcode.letw:
|
||||
case Opcode.sync_leti:
|
||||
if (args[0].value === register) {
|
||||
values.set_value(args[1].value);
|
||||
}
|
||||
break;
|
||||
case Opcode.leta:
|
||||
case Opcode.leto:
|
||||
if (args[0].value === register) {
|
||||
values.set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE);
|
||||
}
|
||||
break;
|
||||
case Opcode.set:
|
||||
if (args[0].value === register) {
|
||||
values.set_value(1);
|
||||
@ -70,7 +77,7 @@ function find_values(block: BasicBlock, end: number, register: number): ValueSet
|
||||
break;
|
||||
case Opcode.rev:
|
||||
if (args[0].value === register) {
|
||||
const prev_vals = find_values(block, i, register);
|
||||
const prev_vals = find_values(new Set(path), block, i, register);
|
||||
const prev_size = prev_vals.size();
|
||||
|
||||
if (prev_size === 0 || (prev_size === 1 && prev_vals.get(0) === 0)) {
|
||||
@ -84,45 +91,89 @@ function find_values(block: BasicBlock, end: number, register: number): ValueSet
|
||||
break;
|
||||
case Opcode.addi:
|
||||
if (args[0].value === register) {
|
||||
values = find_values(block, i, register);
|
||||
values = find_values(new Set(path), block, i, register);
|
||||
values.scalar_add(args[1].value);
|
||||
}
|
||||
break;
|
||||
case Opcode.subi:
|
||||
if (args[0].value === register) {
|
||||
values = find_values(block, i, register);
|
||||
values = find_values(new Set(path), block, i, register);
|
||||
values.scalar_sub(args[1].value);
|
||||
}
|
||||
break;
|
||||
case Opcode.muli:
|
||||
if (args[0].value === register) {
|
||||
values = find_values(block, i, register);
|
||||
values = find_values(new Set(path), block, i, register);
|
||||
values.scalar_mul(args[1].value);
|
||||
}
|
||||
break;
|
||||
case Opcode.divi:
|
||||
if (args[0].value === register) {
|
||||
values = find_values(block, i, register);
|
||||
values = find_values(new Set(path), block, i, register);
|
||||
values.scalar_div(args[1].value);
|
||||
}
|
||||
break;
|
||||
case Opcode.if_zone_clear:
|
||||
if (args[0].value === register) {
|
||||
values.set_interval(0, 1);
|
||||
}
|
||||
break;
|
||||
case Opcode.get_difflvl:
|
||||
case Opcode.get_slotnumber:
|
||||
if (args[0].value === register) {
|
||||
values.set_interval(0, 3);
|
||||
}
|
||||
break;
|
||||
case Opcode.get_random:
|
||||
if (args[1].value === register) {
|
||||
// TODO: undefined values.
|
||||
const min = find_values(block, i, args[0].value).min() || 0;
|
||||
const min = find_values(new Set(path), block, i, args[0].value).min() || 0;
|
||||
const max = Math.max(
|
||||
find_values(block, i, args[0].value + 1).max() || 0,
|
||||
find_values(new Set(path), block, i, args[0].value + 1).max() || 0,
|
||||
min + 1
|
||||
);
|
||||
values.set_interval(min, max - 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Assume any other opcodes that write to the register can produce any value.
|
||||
{
|
||||
const params = instruction.opcode.params;
|
||||
const len = Math.min(args.length, params.length);
|
||||
|
||||
for (let j = 0; j < len; j++) {
|
||||
const param = params[j];
|
||||
const val = args[j].value;
|
||||
|
||||
if (param.write) {
|
||||
if (
|
||||
(param.type instanceof RegTupRefType &&
|
||||
register >= val &&
|
||||
register < val + param.type.types.length) ||
|
||||
(param.type === TYPE_REG_REF && val.includes(register)) ||
|
||||
(param.type === TYPE_REG_REF_VAR && val.includes(register))
|
||||
) {
|
||||
values.set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (values.size() === 0) {
|
||||
path.add(block);
|
||||
|
||||
for (const from of block.from) {
|
||||
values.union(find_values(from, from.end, register));
|
||||
// Bail out from loops.
|
||||
if (path.has(from)) {
|
||||
values.set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE);
|
||||
break;
|
||||
}
|
||||
|
||||
values.union(find_values(new Set(path), from, from.end, register));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ function args_to_strings(params: Param[], args: Arg[]): string[] {
|
||||
}
|
||||
|
||||
break;
|
||||
case Type.Register:
|
||||
case Type.RegRef:
|
||||
arg_strings.push("r" + arg.value);
|
||||
break;
|
||||
case Type.String:
|
||||
|
Loading…
Reference in New Issue
Block a user