Files
scummvm-cursorfix/engines/mediastation/mediascript/function.cpp
2026-02-02 04:50:13 +01:00

568 lines
17 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "mediastation/mediascript/function.h"
#include "mediastation/debugchannels.h"
#include "mediastation/mediastation.h"
namespace MediaStation {
ScriptFunction::ScriptFunction(Chunk &chunk) {
_contextId = chunk.readTypedUint16();
// In PROFILE._ST (only present in some titles), the function ID is reported
// with 19900 added, so function 100 would be reported as 20000. But in
// bytecode, the zero-based ID is used, so that's what we'll store here.
_id = chunk.readTypedUint16();
_code = new CodeChunk(chunk);
}
ScriptFunction::~ScriptFunction() {
delete _code;
_code = nullptr;
}
ScriptValue ScriptFunction::execute(Common::Array<ScriptValue> &args) {
debugC(5, kDebugScript, "\n********** SCRIPT FUNCTION %d **********", _id);
ScriptValue returnValue = _code->execute(&args);
debugC(5, kDebugScript, "********** END SCRIPT FUNCTION **********");
return returnValue;
}
FunctionManager::~FunctionManager() {
for (auto it = _functions.begin(); it != _functions.end(); ++it) {
delete it->_value;
}
_functions.clear();
}
bool FunctionManager::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
bool handledParam = true;
switch (sectionType) {
case 0x31: {
ScriptFunction *function = new ScriptFunction(chunk);
_functions.setVal(function->_id, function);
break;
}
default:
handledParam = false;
}
return handledParam;
}
ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &args) {
ScriptValue returnValue;
// The original had a complex function registration system that I deemed too uselessly complex to
// reimplement. First, we try executing the title-defined function. We try this first because
// later engine versions used some functions IDs that previously mapped to built-in functions in
// earlier engine versions. So we will try executing the title-defined function first and only then
// fall back to the built-in functions.
ScriptFunction *scriptFunction = _functions.getValOrDefault(functionId);
if (scriptFunction != nullptr) {
returnValue = scriptFunction->execute(args);
return returnValue;
}
// If there was no title-defined function, next check for built-in functions.
switch (functionId) {
case kRandomFunction:
case kLegacy_RandomFunction:
assert(args.size() == 2);
script_Random(args, returnValue);
break;
case kTimeOfDayFunction:
case kLegacy_TimeOfDayFunction:
script_TimeOfDay(args, returnValue);
break;
case kEffectTransitionFunction:
case kLegacy_EffectTransitionFunction:
g_engine->getDisplayManager()->effectTransition(args);
break;
case kEffectTransitionOnSyncFunction:
case kLegacy_EffectTransitionOnSyncFunction:
g_engine->getDisplayManager()->setTransitionOnSync(args);
break;
case kPlatformFunction:
case kLegacy_PlatformFunction:
assert(args.empty());
script_GetPlatform(args, returnValue);
break;
case kSquareRootFunction:
case kLegacy_SquareRootFunction:
assert(args.size() == 1);
script_SquareRoot(args, returnValue);
break;
case kGetUniqueRandomFunction:
case kLegacy_GetUniqueRandomFunction:
assert(args.size() >= 2);
script_GetUniqueRandom(args, returnValue);
break;
case kCurrentRunTimeFunction:
script_CurrentRunTime(args, returnValue);
break;
case kSetGammaCorrectionFunction:
script_SetGammaCorrection(args, returnValue);
break;
case kGetDefaultGammaCorrectionFunction:
script_GetDefaultGammaCorrection(args, returnValue);
break;
case kGetCurrentGammaCorrectionFunction:
script_GetCurrentGammaCorrection(args, returnValue);
break;
case kSetAudioVolumeFunction:
assert(args.size() == 1);
script_SetAudioVolume(args, returnValue);
break;
case kGetAudioVolumeFunction:
assert(args.empty());
script_GetAudioVolume(args, returnValue);
break;
case kSystemLanguagePreferenceFunction:
case kLegacy_SystemLanguagePreferenceFunction:
script_SystemLanguagePreference(args, returnValue);
break;
case kSetRegistryFunction:
script_SetRegistry(args, returnValue);
break;
case kGetRegistryFunction:
script_GetRegistry(args, returnValue);
break;
case kSetProfileFunction:
script_SetProfile(args, returnValue);
break;
case kMazeGenerateFunction:
script_MazeGenerate(args, returnValue);
break;
case kMazeApplyMoveMaskFunction:
script_MazeApplyMoveMask(args, returnValue);
break;
case kMazeSolveFunction:
script_MazeSolve(args, returnValue);
break;
case kBeginTimedIntervalFunction:
script_BeginTimedInterval(args, returnValue);
break;
case kEndTimedIntervalFunction:
script_EndTimedInterval(args, returnValue);
break;
case kDrawingFunction:
script_Drawing(args, returnValue);
break;
case kLegacy_DebugPrintFunction:
script_DebugPrint(args, returnValue);
break;
default:
// If we got here, that means there was neither a title-defined nor a built-in function
// for this ID, so we can now declare it unimplemented. This is a warning instead of an error
// so execution can continue, but if the function is expected to return anything, there will
// likely be an error about attempting to assign a null value to a variable.
warning("%s: Unimplemented function 0x%02x", __func__, functionId);
}
return returnValue;
}
void FunctionManager::script_GetPlatform(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
Common::Platform platform = g_engine->getPlatform();
switch (platform) {
case Common::Platform::kPlatformWindows:
returnValue.setToParamToken(kPlatformParamTokenWindows);
break;
case Common::Platform::kPlatformMacintosh:
returnValue.setToParamToken(kPlatformParamTokenWindows);
break;
default:
warning("%s: Unknown platform %d", __func__, static_cast<int>(platform));
returnValue.setToParamToken(kPlatformParamTokenUnknown);
}
}
void FunctionManager::script_Random(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
// This function takes in a range, and then generates a random value within that range.
ScriptValue bottomArg = args[0];
ScriptValue topArg = args[1];
if (bottomArg.getType() != topArg.getType()) {
error("%s: Both arguments must be of same type", __func__);
}
ScriptValueType type = args[0].getType();
double bottom = 0.0;
double top = 0.0;
bool treatAsInteger = false;
switch (type) {
case kScriptValueTypeFloat: {
// For numeric values, treat them as integers (floor values).
bottom = floor(bottomArg.asFloat());
top = floor(topArg.asFloat());
treatAsInteger = true;
break;
}
case kScriptValueTypeBool: {
// Convert boolean values to numbers.
bottom = bottomArg.asBool() ? 1.0 : 0.0;
top = topArg.asBool() ? 1.0 : 0.0;
treatAsInteger = true;
break;
}
case kScriptValueTypeTime: {
// Treat time values as capable of having fractional seconds.
bottom = bottomArg.asTime();
top = topArg.asTime();
treatAsInteger = false;
break;
}
default:
error("%s: Invalid argument type: %s", __func__, scriptValueTypeToStr(type));
}
// Ensure proper inclusive ordering of bottom and top.
if (top < bottom) {
SWAP(top, bottom);
}
// Calculate random value in range.
double range = top - bottom;
uint randomValue = g_engine->_randomSource.getRandomNumber(UINT32_MAX);
double randomFloat = (static_cast<double>(randomValue) * range) / static_cast<double>(UINT32_MAX) + bottom;
if (treatAsInteger) {
randomFloat = floor(randomFloat);
}
// Set result based on original argument type.
switch (type) {
case kScriptValueTypeFloat:
returnValue.setToFloat(randomFloat);
break;
case kScriptValueTypeBool: {
bool boolResult = (randomFloat != 0.0);
returnValue.setToBool(boolResult);
break;
}
case kScriptValueTypeTime:
returnValue.setToTime(randomFloat);
break;
default:
error("%s: Invalid argument type: %s", __func__, scriptValueTypeToStr(type));
}
}
void FunctionManager::script_TimeOfDay(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: TimeOfDay");
}
void FunctionManager::script_SquareRoot(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
if (args[0].getType() != kScriptValueTypeFloat) {
error("%s: Numeric value required", __func__);
}
double value = args[0].asFloat();
if (value < 0.0) {
error("%s: Argument must be nonnegative", __func__);
}
double result = sqrt(value);
returnValue.setToFloat(result);
}
void FunctionManager::script_GetUniqueRandom(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
// Unlike the regular Random which simply returns any random number in a range, GetUniqueRandom allows the caller
// to specify numbers that should NOT be returned (the third arg and onward), making it useful for generating random
// values that haven't been used before or avoiding specific unwanted values.
for (ScriptValue arg : args) {
if (arg.getType() != kScriptValueTypeFloat) {
error("%s: All arguments must be numeric", __func__);
}
}
// The original forces that the list of excluded numbers (and the range to choose from)
// can be at max 100 numbers. With the two args for the range, the max is thus 102.
const uint MAX_ARGS_SIZE = 102;
if (args.size() > MAX_ARGS_SIZE) {
args.resize(MAX_ARGS_SIZE);
}
// Ensure that the range is properly constructed.
double bottom = floor(args[0].asFloat());
double top = floor(args[1].asFloat());
if (top < bottom) {
SWAP(top, bottom);
}
// Build list of unused (non-excluded) numbers in the range. For this numeric type,
// everything is treated as an integer (even though it's stored as a double).
Common::Array<double> unusedNumbers;
for (double currentValue = bottom; currentValue < top; currentValue += 1.0) {
// Check if this value appears in the exclusion list (args 2 onwards).
bool isExcluded = false;
for (uint i = 2; i < args.size(); i++) {
if (args[i].asFloat() == currentValue) {
isExcluded = true;
break;
}
}
if (!isExcluded) {
unusedNumbers.push_back(currentValue);
}
}
if (unusedNumbers.size() > 0) {
uint randomIndex = g_engine->_randomSource.getRandomNumberRng(0, unusedNumbers.size());
returnValue.setToFloat(unusedNumbers[randomIndex]);
} else {
warning("%s: No unused numbers to choose from", __func__);
}
}
void FunctionManager::script_CurrentRunTime(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
// The current runtime is expected to be returned in seconds.
const uint MILLISECONDS_IN_ONE_SECOND = 1000;
double runtimeInSeconds = g_system->getMillis() / MILLISECONDS_IN_ONE_SECOND;
returnValue.setToFloat(runtimeInSeconds);
}
void FunctionManager::script_SetGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
if (args.size() != 1 && args.size() != 3) {
warning("%s: Expected 1 or 3 arguments, got %u", __func__, args.size());
return;
}
double red = 1.0;
double green = 1.0;
double blue = 1.0;
if (args.size() >= 3) {
if (args[0].getType() != kScriptValueTypeFloat ||
args[1].getType() != kScriptValueTypeFloat ||
args[2].getType() != kScriptValueTypeFloat) {
warning("%s: Expected float arguments", __func__);
return;
}
red = args[0].asFloat();
green = args[1].asFloat();
blue = args[2].asFloat();
} else if (args.size() >= 1) {
if (args[0].getType() != kScriptValueTypeCollection) {
warning("%s: Expected collection argument", __func__);
return;
}
Common::SharedPtr<Collection> collection = args[0].asCollection();
if (collection->size() != 3) {
warning("%s: Collection must contain exactly 3 elements, got %u", __func__, collection->size());
return;
}
if (collection->operator[](0).getType() != kScriptValueTypeFloat ||
collection->operator[](1).getType() != kScriptValueTypeFloat ||
collection->operator[](2).getType() != kScriptValueTypeFloat) {
warning("%s: Expected float arguments", __func__);
return;
}
red = collection->operator[](0).asFloat();
green = collection->operator[](1).asFloat();
blue = collection->operator[](2).asFloat();
}
g_engine->getDisplayManager()->setGammaValues(red, green, blue);
}
void FunctionManager::script_GetDefaultGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
if (args.size() != 0) {
warning("%s: Expected 0 arguments, got %u", __func__, args.size());
return;
}
double red, green, blue;
g_engine->getDisplayManager()->getDefaultGammaValues(red, green, blue);
Common::SharedPtr<Collection> collection = Common::SharedPtr<Collection>(new Collection());
ScriptValue redValue;
redValue.setToFloat(red);
collection->push_back(redValue);
ScriptValue greenValue;
greenValue.setToFloat(green);
collection->push_back(greenValue);
ScriptValue blueValue;
blueValue.setToFloat(blue);
collection->push_back(blueValue);
returnValue.setToCollection(collection);
}
void FunctionManager::script_GetCurrentGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
if (args.size() != 0) {
warning("%s: Expected 0 arguments, got %u", __func__, args.size());
return;
}
double red, green, blue;
g_engine->getDisplayManager()->getGammaValues(red, green, blue);
Common::SharedPtr<Collection> collection = Common::SharedPtr<Collection>(new Collection());
ScriptValue redValue;
redValue.setToFloat(red);
collection->push_back(redValue);
ScriptValue greenValue;
greenValue.setToFloat(green);
collection->push_back(greenValue);
ScriptValue blueValue;
blueValue.setToFloat(blue);
collection->push_back(blueValue);
returnValue.setToCollection(collection);
}
void FunctionManager::script_SetAudioVolume(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
if (args[0].getType() != kScriptValueTypeFloat) {
warning("%s: Expected float argument", __func__);
return;
}
// Convert from 0.0 - 1.0 to ScummVM's mixer range.
double volume = args[0].asFloat();
volume = CLIP(volume, 0.0, 1.0);
int mixerVolume = static_cast<int>(volume * Audio::Mixer::kMaxMixerVolume);
g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, mixerVolume);
}
void FunctionManager::script_GetAudioVolume(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
// Convert from ScummVM's mixer range to 0.0 - 1.0.
int mixerVolume = g_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kPlainSoundType);
double volume = static_cast<double>(mixerVolume) / static_cast<double>(Audio::Mixer::kMaxMixerVolume);
CLIP(volume, 0.0, 1.0);
returnValue.setToFloat(volume);
}
void FunctionManager::script_SystemLanguagePreference(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: SystemLanguagePreference");
}
void FunctionManager::script_SetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: SetRegistry");
}
void FunctionManager::script_GetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: GetRegistry");
}
void FunctionManager::script_SetProfile(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: SetProfile");
}
void FunctionManager::script_DebugPrint(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
// The original reports time in seconds, but milliseconds is fine.
// The "IMT @ clock ..." format is from the original's debug printing style.
Common::String output = Common::String::format("IMT @ clock %d", g_system->getMillis());
for (uint i = 0; i < args.size(); i++) {
// Append all provided arguments.
if (i != 0) {
output += ", ";
} else {
output += " ";
}
output += args[i].getDebugString();
}
debug("%s", output.c_str());
}
void FunctionManager::script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: MazeGenerate");
}
void FunctionManager::script_MazeApplyMoveMask(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: MazeApplyMoveMask");
}
void FunctionManager::script_MazeSolve(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: MazeSolve");
}
void FunctionManager::script_BeginTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: BeginTimedInterval");
}
void FunctionManager::script_EndTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: EndTimedInterval");
}
void FunctionManager::script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
warning("STUB: Drawing");
}
void FunctionManager::deleteFunctionsForContext(uint contextId) {
// Collect function IDs to delete first.
Common::Array<ScriptFunction *> functionsToDelete;
for (auto it = _functions.begin(); it != _functions.end(); ++it) {
ScriptFunction *scriptFunction = it->_value;
if (scriptFunction->_contextId == contextId) {
functionsToDelete.push_back(scriptFunction);
}
}
// Now delete them.
for (ScriptFunction *scriptFunction : functionsToDelete) {
_functions.erase(scriptFunction->_id);
delete scriptFunction;
}
}
} // End of namespace MediaStation