/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "common/ptr.h"
#include "mediastation/mediastation.h"
#include "mediastation/mediascript/codechunk.h"
#include "mediastation/mediascript/collection.h"
#include "mediastation/debugchannels.h"
namespace MediaStation {
CodeChunk::CodeChunk(Chunk &chunk) {
uint lengthInBytes = chunk.readTypedUint32();
debugC(5, kDebugLoading, "CodeChunk::CodeChunk(): Length 0x%x (@0x%llx)", lengthInBytes, static_cast(chunk.pos()));
_bytecode = static_cast(chunk.readStream(lengthInBytes));
}
ScriptValue CodeChunk::executeNextBlock() {
uint blockSize = _bytecode->readTypedUint32();
int64 startingPos = _bytecode->pos();
debugC(7, kDebugScript, "%s: Entering new block (blockSize: %d, startingPos: %lld)",
__func__, blockSize, static_cast(startingPos));
ScriptValue returnValue;
ExpressionType expressionType = static_cast(_bytecode->readTypedUint16());
while (expressionType != kExpressionTypeEmpty && !_returnImmediately) {
returnValue = evaluateExpression(expressionType);
expressionType = static_cast(_bytecode->readTypedUint16());
if (expressionType == kExpressionTypeEmpty) {
debugC(7, kDebugScript, "%s: Done executing block due to end of chunk", __func__);
}
if (_returnImmediately) {
debugC(7, kDebugScript, "%s: Done executing block due to script requesting immediate return", __func__);
}
}
// Verify we consumed the right number of script bytes.
if (!_returnImmediately) {
uint bytesRead = _bytecode->pos() - startingPos;
if (bytesRead != blockSize) {
error("%s: Expected to have read %d script bytes, actually read %d", __func__, blockSize, bytesRead);
}
}
return returnValue;
}
void CodeChunk::skipNextBlock() {
uint lengthInBytes = _bytecode->readTypedUint32();
_bytecode->skip(lengthInBytes);
}
ScriptValue CodeChunk::execute(Common::Array *args) {
_args = args;
ScriptValue returnValue = executeNextBlock();
// Rewind the stream once we're finished, in case we need to execute
// this code again!
_bytecode->seek(0);
_returnImmediately = false;
_locals.clear();
// We don't own the args, so we will prevent a potentially out-of-scope
// variable from being re-accessed.
_args = nullptr;
return returnValue;
}
ScriptValue CodeChunk::evaluateExpression() {
ExpressionType expressionType = static_cast(_bytecode->readTypedUint16());
ScriptValue returnValue = evaluateExpression(expressionType);
return returnValue;
}
ScriptValue CodeChunk::evaluateExpression(ExpressionType expressionType) {
debugCN(5, kDebugScript, "(%s) ", expressionTypeToStr(expressionType));
ScriptValue returnValue;
switch (expressionType) {
case kExpressionTypeEmpty:
break;
case kExpressionTypeOperation:
returnValue = evaluateOperation();
break;
case kExpressionTypeValue:
returnValue = evaluateValue();
break;
case kExpressionTypeVariable:
returnValue = evaluateVariable();
break;
default:
error("%s: Got unimplemented expression type %s (%d)", __func__,
expressionTypeToStr(expressionType), static_cast(expressionType));
}
return returnValue;
}
ScriptValue CodeChunk::evaluateOperation() {
Opcode opcode = static_cast(_bytecode->readTypedUint16());
debugCN(5, kDebugScript, "%s ", opcodeToStr(opcode));
ScriptValue returnValue;
switch (opcode) {
case kOpcodeIf:
evaluateIf();
break;
case kOpcodeIfElse:
evaluateIfElse();
break;
case kOpcodeAssignVariable:
evaluateAssign();
break;
case kOpcodeOr:
case kOpcodeXor:
case kOpcodeAnd:
case kOpcodeEquals:
case kOpcodeNotEquals:
case kOpcodeLessThan:
case kOpcodeGreaterThan:
case kOpcodeLessThanOrEqualTo:
case kOpcodeGreaterThanOrEqualTo:
case kOpcodeAdd:
case kOpcodeSubtract:
case kOpcodeMultiply:
case kOpcodeDivide:
case kOpcodeModulo:
returnValue = evaluateBinaryOperation(opcode);
break;
case kOpcodeNegate:
returnValue = evaluateUnaryOperation();
break;
case kOpcodeCallFunction:
returnValue = evaluateFunctionCall();
break;
case kOpcodeCallMethod:
returnValue = evaluateMethodCall();
break;
case kOpcodeDeclareLocals:
evaluateDeclareLocals();
break;
case kOpcodeReturn:
returnValue = evaluateReturn();
break;
case kOpcodeReturnNoValue:
evaluateReturnNoValue();
break;
case kOpcodeWhile:
evaluateWhileLoop();
break;
case kOpcodeCallFunctionInVariable:
returnValue = evaluateFunctionCall(true);
break;
case kOpcodeCallMethodInVariable:
returnValue = evaluateMethodCall(true);
break;
default:
error("%s: Got unimplemented opcode %s (%d)", __func__, opcodeToStr(opcode), static_cast(opcode));
}
return returnValue;
}
ScriptValue CodeChunk::evaluateValue() {
OperandType operandType = static_cast(_bytecode->readTypedUint16());
debugCN(5, kDebugScript, "%s ", operandTypeToStr(operandType));
ScriptValue returnValue;
switch (operandType) {
case kOperandTypeBool: {
int b = _bytecode->readTypedByte();
if (b != 0 && b != 1) {
error("%s: Got invalid literal bool value %d", __func__, b);
}
debugC(5, kDebugScript, "%d ", b);
returnValue.setToBool(b == 1 ? true : false);
return returnValue;
}
case kOperandTypeFloat: {
double f = _bytecode->readTypedDouble();
debugC(5, kDebugScript, "%f ", f);
returnValue.setToFloat(f);
return returnValue;
}
case kOperandTypeInt: {
int i = _bytecode->readTypedSint32();
debugC(5, kDebugScript, "%d ", i);
// Ints are stored internally as doubles.
returnValue.setToFloat(static_cast(i));
return returnValue;
}
case kOperandTypeString: {
// This is indeed a raw string, not a string wrapped in a datum!
uint size = _bytecode->readTypedUint16();
Common::String string = _bytecode->readString('\0', size);
debugC(5, kDebugScript, "%s ", string.c_str());
returnValue.setToString(string);
return returnValue;
}
case kOperandTypeParamToken: {
uint literal = _bytecode->readTypedUint16();
debugC(5, kDebugScript, "%d ", literal);
returnValue.setToParamToken(literal);
return returnValue;
}
case kOperandTypeActorId: {
uint actorId = _bytecode->readTypedUint16();
debugC(5, kDebugScript, "%d ", actorId);
returnValue.setToActorId(actorId);
return returnValue;
}
case kOperandTypeTime: {
double d = _bytecode->readTypedTime();
debugC(5, kDebugScript, "%f ", d);
returnValue.setToTime(d);
return returnValue;
}
case kOperandTypeVariable: {
returnValue = ScriptValue(_bytecode);
return returnValue;
}
case kOperandTypeFunctionId: {
uint functionId = _bytecode->readTypedUint16();
debugC(5, kDebugScript, "%d ", functionId);
returnValue.setToFunctionId(functionId);
return returnValue;
}
case kOperandTypeMethodId: {
BuiltInMethod methodId = static_cast(_bytecode->readTypedUint16());
debugC(5, kDebugScript, "%s ", builtInMethodToStr(methodId));
returnValue.setToMethodId(methodId);
return returnValue;
}
default:
error("%s: Got unknown ScriptValue type %s (%d)", __func__, operandTypeToStr(operandType), static_cast(operandType));
}
}
ScriptValue CodeChunk::evaluateVariable() {
ScriptValue *variable = readAndReturnVariable();
return *variable;
}
ScriptValue *CodeChunk::readAndReturnVariable() {
uint id = _bytecode->readTypedUint16();
VariableScope scope = static_cast(_bytecode->readTypedUint16());
debugC(5, kDebugScript, "%d (%s)", id, variableScopeToStr(scope));
ScriptValue returnValue;
switch (scope) {
case kVariableScopeGlobal: {
ScriptValue *variable = g_engine->getVariable(id);
if (variable == nullptr) {
error("%s: Global variable %d doesn't exist", __func__, id);
}
return variable;
}
case kVariableScopeLocal: {
uint index = id - 1;
return &_locals.operator[](index);
}
case kVariableScopeIndirectParameter: {
ScriptValue indexValue = evaluateExpression();
uint index = static_cast(indexValue.asFloat() + id);
return &_args->operator[](index);
}
case kVariableScopeParameter: {
uint index = id - 1;
if (_args == nullptr) {
error("%s: Requested a parameter in a code chunk that has no parameters", __func__);
}
return &_args->operator[](index);
}
default:
error("%s: Got unknown variable scope %s (%d)", __func__, variableScopeToStr(scope), static_cast(scope));
}
}
void CodeChunk::evaluateIf() {
debugCN(5, kDebugScript, "\n condition: ");
ScriptValue condition = evaluateExpression();
if (condition.getType() != kScriptValueTypeBool) {
error("%s: Expected bool condition, got %s", __func__, scriptValueTypeToStr(condition.getType()));
}
if (condition.asBool()) {
executeNextBlock();
} else {
skipNextBlock();
}
}
void CodeChunk::evaluateIfElse() {
debugCN(5, kDebugScript, "\n condition: ");
ScriptValue condition = evaluateExpression();
if (condition.getType() != kScriptValueTypeBool) {
error("%s: Expected bool condition, got %s", __func__, scriptValueTypeToStr(condition.getType()));
}
if (condition.asBool()) {
executeNextBlock();
skipNextBlock();
} else {
skipNextBlock();
executeNextBlock();
}
}
ScriptValue CodeChunk::evaluateAssign() {
debugCN(5, kDebugScript, "Variable ");
ScriptValue *targetVariable = readAndReturnVariable();
debugC(5, kDebugScript, " Value: ");
ScriptValue value = evaluateExpression();
if (value.getType() == kScriptValueTypeEmpty) {
error("%s: Attempt to assign an empty value to a variable", __func__);
}
if (targetVariable != nullptr) {
*targetVariable = value;
return value;
} else {
error("%s: Attempt to assign to null variable", __func__);
}
}
ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
debugCN(5, kDebugScript, "\n lhs: ");
ScriptValue value1 = evaluateExpression();
debugCN(5, kDebugScript, " rhs: ");
ScriptValue value2 = evaluateExpression();
ScriptValue returnValue;
switch (op) {
case kOpcodeOr:
returnValue.setToBool(value1 || value2);
break;
case kOpcodeXor:
returnValue.setToBool(value1 ^ value2);
break;
case kOpcodeAnd:
returnValue.setToBool(value1 && value2);
break;
case kOpcodeEquals:
returnValue.setToBool(value1 == value2);
break;
case kOpcodeNotEquals:
returnValue.setToBool(value1 != value2);
break;
case kOpcodeLessThan:
returnValue.setToBool(value1 < value2);
break;
case kOpcodeGreaterThan:
returnValue.setToBool(value1 > value2);
break;
case kOpcodeLessThanOrEqualTo:
returnValue.setToBool(value1 <= value2);
break;
case kOpcodeGreaterThanOrEqualTo:
returnValue.setToBool(value1 >= value2);
break;
case kOpcodeAdd:
returnValue = value1 + value2;
break;
case kOpcodeSubtract:
returnValue = value1 - value2;
break;
case kOpcodeMultiply:
returnValue = value1 * value2;
break;
case kOpcodeDivide:
returnValue = value1 / value2;
break;
case kOpcodeModulo:
returnValue = value1 % value2;
break;
default:
error("%s: Got unknown binary operation opcode %s", __func__, opcodeToStr(op));
}
return returnValue;
}
ScriptValue CodeChunk::evaluateUnaryOperation() {
// The only supported unary operation seems to be negation.
ScriptValue value = evaluateExpression();
debugCN(5, kDebugScript, " value: ");
return -value;
}
ScriptValue CodeChunk::evaluateFunctionCall(bool isIndirect) {
uint functionId, paramCount = 0;
if (isIndirect) {
paramCount = _bytecode->readTypedUint16();
ScriptValue value = evaluateExpression();
functionId = value.asFunctionId();
} else {
functionId = _bytecode->readTypedUint16();
paramCount = _bytecode->readTypedUint16();
}
return evaluateFunctionCall(functionId, paramCount);
}
ScriptValue CodeChunk::evaluateFunctionCall(uint functionId, uint paramCount) {
debugC(5, kDebugScript, "%d (%d params)", functionId, paramCount);
Common::Array args;
for (uint i = 0; i < paramCount; i++) {
debugCN(5, kDebugScript, " Param %d: ", i);
ScriptValue arg = evaluateExpression();
args.push_back(arg);
}
ScriptValue returnValue = g_engine->getFunctionManager()->call(functionId, args);
return returnValue;
}
ScriptValue CodeChunk::evaluateMethodCall(bool isIndirect) {
BuiltInMethod method;
uint paramCount = 0;
if (isIndirect) {
paramCount = _bytecode->readTypedUint16();
ScriptValue value = evaluateExpression();
method = value.asMethodId();
} else {
method = static_cast(_bytecode->readTypedUint16());
paramCount = _bytecode->readTypedUint16();
}
return evaluateMethodCall(method, paramCount);
}
ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount) {
// In Media Station, all methods are built-in - there aren't
// custom objects or methods individual titles can
// define. Functions, however, CAN be title-defined.
// But here, we're only looking for built-in methods.
debugC(5, kDebugScript, "%s (%d params)", builtInMethodToStr(method), paramCount);
debugCN(5, kDebugScript, " Self: ");
ScriptValue target = evaluateExpression();
Common::Array args;
for (uint i = 0; i < paramCount; i++) {
debugCN(5, kDebugScript, " Param %d: ", i);
ScriptValue arg = evaluateExpression();
args.push_back(arg);
}
ScriptValue returnValue;
switch (target.getType()) {
case kScriptValueTypeActorId: {
if (target.asActorId() == 0) {
// It seems to be valid to call a method on a null actor ID, in
// which case nothing happens. Still issue warning for traceability.
warning("%s: Attempt to call method on a null actor ID", __func__);
return returnValue;
} else {
// This is a regular actor that we can process directly.
uint actorId = target.asActorId();
Actor *targetActor = g_engine->getActorById(actorId);
if (targetActor == nullptr) {
error("%s: Attempt to call method on actor ID %d, which isn't loaded", __func__, target.asActorId());
}
returnValue = targetActor->callMethod(method, args);
return returnValue;
}
}
case kScriptValueTypeCollection: {
Common::SharedPtr collection = target.asCollection();
returnValue = collection->callMethod(method, args);
return returnValue;
}
default:
error("Attempt to call method on unimplemented value type %s (%d)",
scriptValueTypeToStr(target.getType()), static_cast(target.getType()));
}
}
void CodeChunk::evaluateDeclareLocals() {
uint localVariableCount = _bytecode->readTypedUint16();
if (localVariableCount <= 0) {
error("Got non-positive local variable count");
}
debugC(5, kDebugScript, "%d", localVariableCount);
_locals = Common::Array(localVariableCount);
}
ScriptValue CodeChunk::evaluateReturn() {
ScriptValue returnValue = evaluateExpression();
_returnImmediately = true;
return returnValue;
}
void CodeChunk::evaluateReturnNoValue() {
_returnImmediately = true;
}
void CodeChunk::evaluateWhileLoop() {
uint loopStartPosition = _bytecode->pos();
uint iterationCount = 0;
while (true) {
// Seek to the top of the loop bytecode.
_bytecode->seek(loopStartPosition);
ScriptValue condition = evaluateExpression();
if (condition.getType() != kScriptValueTypeBool) {
error("Expected loop condition to be bool, not %s", scriptValueTypeToStr(condition.getType()));
}
if (++iterationCount >= MAX_LOOP_ITERATION_COUNT) {
error("Exceeded max loop iteration count");
}
if (condition.asBool()) {
executeNextBlock();
} else {
skipNextBlock();
break;
}
}
}
CodeChunk::~CodeChunk() {
_locals.clear();
// We don't own the args, so we don't need to delete it.
_args = nullptr;
delete _bytecode;
_bytecode = nullptr;
}
} // End of namespace MediaStation