/* 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