From 62accd5196108a55b986f3ba7828560975a24e5b Mon Sep 17 00:00:00 2001 From: elasota Date: Fri, 13 May 2022 17:44:17 -0400 Subject: [PATCH] MTROPOLIS: Load data for Obsidian word games --- engines/mtropolis/miniscript.cpp | 30 +++ engines/mtropolis/miniscript.h | 4 +- engines/mtropolis/modifiers.cpp | 32 ++- engines/mtropolis/modifiers.h | 4 + engines/mtropolis/mtropolis.cpp | 101 +++++++- engines/mtropolis/plugin/obsidian.cpp | 263 ++++++++++++++++++++- engines/mtropolis/plugin/obsidian.h | 80 ++++++- engines/mtropolis/plugin/obsidian_data.cpp | 17 ++ engines/mtropolis/plugin/obsidian_data.h | 13 + engines/mtropolis/plugin/standard.cpp | 6 + engines/mtropolis/plugin/standard.h | 3 +- engines/mtropolis/plugins.h | 8 +- engines/mtropolis/runtime.cpp | 29 ++- engines/mtropolis/runtime.h | 3 + 14 files changed, 579 insertions(+), 14 deletions(-) diff --git a/engines/mtropolis/miniscript.cpp b/engines/mtropolis/miniscript.cpp index cd1f53d7848..201c6c869b5 100644 --- a/engines/mtropolis/miniscript.cpp +++ b/engines/mtropolis/miniscript.cpp @@ -856,6 +856,36 @@ MiniscriptInstructionOutcome Or::execute(MiniscriptThread *thread) const { return kMiniscriptInstructionOutcomeContinue; } +MiniscriptInstructionOutcome Neg::execute(MiniscriptThread *thread) const { + if (thread->getStackSize() < 1) { + thread->error("Stack underflow"); + return kMiniscriptInstructionOutcomeFailed; + } + + MiniscriptInstructionOutcome outcome = thread->dereferenceRValue(0, false); + if (outcome != kMiniscriptInstructionOutcomeContinue) + return outcome; + + DynamicValue &value = thread->getStackValueFromTop(0).value; + switch (value.getType()) { + case DynamicValueTypes::kFloat: + value.setFloat(-value.getFloat()); + break; + case DynamicValueTypes::kInteger: { + int32 i = value.getInt(); + if (i == (0 - 1 -0x7fffffff)) + value.setFloat(-static_cast(i)); + else + value.setInt(-i); + } break; + default: + thread->error("Couldn't negate a value of a non-numeric type"); + return kMiniscriptInstructionOutcomeFailed; + } + + return kMiniscriptInstructionOutcomeContinue; +} + MiniscriptInstructionOutcome Not::execute(MiniscriptThread *thread) const { if (thread->getStackSize() < 1) { thread->error("Stack underflow"); diff --git a/engines/mtropolis/miniscript.h b/engines/mtropolis/miniscript.h index dd51a7bc730..159b2a97cdb 100644 --- a/engines/mtropolis/miniscript.h +++ b/engines/mtropolis/miniscript.h @@ -159,7 +159,9 @@ namespace MiniscriptInstructions { MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override; }; - class Neg : public UnimplementedInstruction { + class Neg : public MiniscriptInstruction { + private: + MiniscriptInstructionOutcome execute(MiniscriptThread *thread) const override; }; class Not : public MiniscriptInstruction { diff --git a/engines/mtropolis/modifiers.cpp b/engines/mtropolis/modifiers.cpp index b5dc7246186..5ad82cfb556 100644 --- a/engines/mtropolis/modifiers.cpp +++ b/engines/mtropolis/modifiers.cpp @@ -1315,7 +1315,13 @@ bool IntegerVariableModifier::varSetValue(MiniscriptThread *thread, const Dynami _value = static_cast(floor(value.getFloat() + 0.5)); else if (value.getType() == DynamicValueTypes::kInteger) _value = value.getInt(); - else + else if (value.getType() == DynamicValueTypes::kString) { + // Should this scan %lf to a double and round it instead? + int i; + if (!sscanf(value.getString().c_str(), "%i", &i)) + return false; + _value = i; + } else return false; return true; @@ -1385,6 +1391,30 @@ void IntegerRangeVariableModifier::varGetValue(MiniscriptThread *thread, Dynamic dest.setIntRange(_range); } +bool IntegerRangeVariableModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) { + if (attrib == "start") { + result.setInt(_range.min); + return true; + } + if (attrib == "end") { + result.setInt(_range.max); + return true; + } + return Modifier::readAttribute(thread, result, attrib); +} + +MiniscriptInstructionOutcome IntegerRangeVariableModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { + if (attrib == "start") { + DynamicValueWriteIntegerHelper::create(&_range.min, result); + return kMiniscriptInstructionOutcomeContinue; + } + if (attrib == "end") { + DynamicValueWriteIntegerHelper::create(&_range.max, result); + return kMiniscriptInstructionOutcomeContinue; + } + return Modifier::writeRefAttribute(thread, result, attrib); +} + #ifdef MTROPOLIS_DEBUG_ENABLE void IntegerRangeVariableModifier::debugInspect(IDebugInspectionReport *report) const { VariableModifier::debugInspect(report); diff --git a/engines/mtropolis/modifiers.h b/engines/mtropolis/modifiers.h index d3263123fc9..dd00a49fe7c 100644 --- a/engines/mtropolis/modifiers.h +++ b/engines/mtropolis/modifiers.h @@ -473,6 +473,7 @@ public: #ifdef MTROPOLIS_DEBUG_ENABLE const char *debugGetTypeName() const override { return "Keyboard Messenger Modifier"; } + SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; } #endif private: @@ -738,6 +739,9 @@ public: bool varSetValue(MiniscriptThread *thread, const DynamicValue &value) override; void varGetValue(MiniscriptThread *thread, DynamicValue &dest) const override; + bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib); + MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib); + #ifdef MTROPOLIS_DEBUG_ENABLE const char *debugGetTypeName() const override { return "Integer Range Variable Modifier"; } SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; } diff --git a/engines/mtropolis/mtropolis.cpp b/engines/mtropolis/mtropolis.cpp index 15809c820e1..2d2903bd1e4 100644 --- a/engines/mtropolis/mtropolis.cpp +++ b/engines/mtropolis/mtropolis.cpp @@ -28,6 +28,7 @@ #include "mtropolis/plugins.h" #include "mtropolis/plugin/standard.h" +#include "mtropolis/plugin/obsidian.h" #include "common/config-manager.h" #include "common/debug.h" @@ -49,6 +50,49 @@ namespace MTropolis { +static Common::SharedPtr loadWinObsidianWordGameData() { + Common::File f; + if (!f.open("RSGKit.r95")) { + error("Couldn't open word game data file"); + return nullptr; + } + + Common::SharedPtr wgData(new Obsidian::WordGameData()); + + Obsidian::WordGameLoadBucket buckets[] = { + {0, 0}, // 0 letters + {0x63D54, 0x63D5C}, // 1 letter + {0x63BF8, 0x63CA4}, // 2 letter + {0x627D8, 0x631E8}, // 3 letter + {0x5C2C8, 0x60628}, // 4 letter + {0x52F4C, 0x5919C}, // 5 letter + {0x47A64, 0x4F2FC}, // 6 letter + {0x3BC98, 0x43B20}, // 7 letter + {0x2DA78, 0x38410}, // 8 letter + {0x218F8, 0x2AA18}, // 9 letter + {0x19D78, 0x1FA18}, // 10 letter + {0x15738, 0x18BE8}, // 11 letter + {0x128A8, 0x14DE8}, // 12 letter + {0x1129C, 0x1243C}, // 13 letter + {0x10974, 0x110C4}, // 14 letter + {0x105EC, 0x108BC}, // 15 letter + {0x10454, 0x105A8}, // 16 letter + {0x103A8, 0x10434}, // 17 letter + {0x10348, 0x10398}, // 18 letter + {0, 0}, // 19 letter + {0x10328, 0x10340}, // 20 letter + {0x102EC, 0x1031C}, // 21 letter + {0x102D0, 0x102E8}, // 22 letter + }; + + if (!wgData->load(&f, buckets, 23, 4, true)) { + error("Failed to load word game data file"); + return nullptr; + } + + return wgData; +} + static bool loadCursorsFromPE(CursorGraphicCollection &cursorGraphics, Common::SeekableReadStream *stream) { Common::SharedPtr winRes(Common::WinResources::createFromEXE(stream)); if (!winRes) { @@ -131,6 +175,7 @@ struct MacObsidianResources : public ProjectResources { void setup(); Common::SeekableReadStream *getSegmentStream(int index) const; const Common::SharedPtr &getCursorGraphics() const; + const Common::SharedPtr &getWordGameData() const; private: Common::MacResManager _installerResMan; @@ -141,6 +186,7 @@ private: Common::SeekableReadStream *_segmentStreams[6]; Common::SharedPtr _cursorGraphics; + Common::SharedPtr _wordGameData; }; MacObsidianResources::MacObsidianResources() : _installerArchive(nullptr), _installerDataForkStream(nullptr) { @@ -197,6 +243,49 @@ void MacObsidianResources::setup() { error("Failed to read cursor resources from file '%s'", fileName); } + debug(1, "Loading word games..."); + + { + Common::ArchiveMemberPtr rsgKit = _installerArchive->getMember(Common::Path("RSGKit.rPP")); + if (!rsgKit) + error("Couldn't find word game file in installer archive"); + + _wordGameData.reset(new Obsidian::WordGameData()); + + Common::SharedPtr stream(rsgKit->createReadStream()); + if (!stream) + error("Failed to open word game file"); + + Obsidian::WordGameLoadBucket buckets[] = { + {0, 0}, // 0 letters + {0xD7C8, 0xD7CC}, // 1 letter + {0xD7CC, 0xD84D}, // 2 letter + {0xD84D, 0xE25D}, // 3 letter + {0x1008C, 0x12AA8}, // 4 letter + {0x14C58, 0x19614}, // 5 letter + {0x1C73C, 0x230C1}, // 6 letter + {0x26D10, 0x2EB98}, // 7 letter + {0x32ADC, 0x3AA0E}, // 8 letter + {0x3E298, 0x45B88}, // 9 letter + {0x48BE8, 0x4E0D0}, // 10 letter + {0x4FFB0, 0x53460}, // 11 letter + {0x545F0, 0x56434}, // 12 letter + {0x56D84, 0x57CF0}, // 13 letter + {0x58158, 0x58833}, // 14 letter + {0x58A08, 0x58CD8}, // 15 letter + {0x58D8C, 0x58EAD}, // 16 letter + {0x58EF4, 0x58F72}, // 17 letter + {0x58F90, 0x58FDC}, // 18 letter + {0, 0}, // 19 letter + {0x58FEC, 0x59001}, // 20 letter + {0x59008, 0x59034}, // 21 letter + {0x5903C, 0x59053}, // 22 letter + }; + + if (!_wordGameData->load(stream.get(), buckets, 23, 1, false)) + error("Failed to load word game data"); + } + debug(1, "Finished unpacking installer resources"); } @@ -204,10 +293,14 @@ Common::SeekableReadStream *MacObsidianResources::getSegmentStream(int index) co return _segmentStreams[index]; } -const Common::SharedPtr& MacObsidianResources::getCursorGraphics() const { +const Common::SharedPtr &MacObsidianResources::getCursorGraphics() const { return _cursorGraphics; } +const Common::SharedPtr& MacObsidianResources::getWordGameData() const { + return _wordGameData; +} + MacObsidianResources::~MacObsidianResources() { for (int i = 0; i < 6; i++) delete _segmentStreams[i]; @@ -307,13 +400,15 @@ Common::Error MTropolisEngine::run() { } } + Common::SharedPtr wgData = loadWinObsidianWordGameData(); + desc->setCursorGraphics(cursors); Common::SharedPtr standardPlugIn = PlugIns::createStandard(); static_cast(standardPlugIn.get())->getHacks().allowGarbledListModData = true; desc->addPlugIn(standardPlugIn); - desc->addPlugIn(PlugIns::createObsidian()); + desc->addPlugIn(PlugIns::createObsidian(wgData)); _runtime->queueProject(desc); @@ -345,7 +440,7 @@ Common::Error MTropolisEngine::run() { static_cast(standardPlugIn.get())->getHacks().allowGarbledListModData = true; desc->addPlugIn(standardPlugIn); - desc->addPlugIn(PlugIns::createObsidian()); + desc->addPlugIn(PlugIns::createObsidian(resources->getWordGameData())); desc->setResources(resPtr); desc->setCursorGraphics(resources->getCursorGraphics()); diff --git a/engines/mtropolis/plugin/obsidian.cpp b/engines/mtropolis/plugin/obsidian.cpp index b157ee2f1df..dd911fe56be 100644 --- a/engines/mtropolis/plugin/obsidian.cpp +++ b/engines/mtropolis/plugin/obsidian.cpp @@ -116,26 +116,281 @@ MiniscriptInstructionOutcome TextWorkModifier::writeRefAttribute(MiniscriptThrea return Modifier::writeRefAttribute(thread, result, attrib); } - Common::SharedPtr TextWorkModifier::shallowClone() const { return Common::SharedPtr(new TextWorkModifier(*this)); } -ObsidianPlugIn::ObsidianPlugIn() : _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this) { +DictionaryModifier::DictionaryModifier() : _plugIn(nullptr) { +} + +bool DictionaryModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::DictionaryModifier &data) { + if (data.str.type != Data::PlugInTypeTaggedValue::kString) + return false; + + _str = data.str.str; + + if (data.index.type != Data::PlugInTypeTaggedValue::kInteger) + return false; + + _index = data.index.value.asInt; + _isIndexResolved = true; + _plugIn = static_cast(context.plugIn); + + return true; +} + + +bool DictionaryModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) { + if (attrib == "index") { + resolveStringIndex(); + result.setInt(_index); + return true; + } + if (attrib == "string") { + result.setString(_str); + return true; + } + + return Modifier::readAttribute(thread, result, attrib); +} + +MiniscriptInstructionOutcome DictionaryModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { + if (attrib == "index") { + DynamicValueWriteFuncHelper::create(this, result); + return kMiniscriptInstructionOutcomeContinue; + } + if (attrib == "string") { + DynamicValueWriteFuncHelper::create(this, result); + return kMiniscriptInstructionOutcomeContinue; + } + + return Modifier::writeRefAttribute(thread, result, attrib); +} + +void DictionaryModifier::resolveStringIndex() { + if (_isIndexResolved) + return; + + _index = 0; + _isIndexResolved = true; + + const Common::Array &wordBuckets = _plugIn->getWordGameData()->getWordBuckets(); + + size_t strLength = _str.size(); + if (strLength >= wordBuckets.size()) + return; + + const WordGameData::WordBucket &bucket = wordBuckets[strLength]; + + size_t lowOffsetInclusive = 0; + size_t highOffsetExclusive = bucket.wordIndexes.size(); + + const char *strChars = _str.c_str(); + + // Binary search + while (lowOffsetInclusive != highOffsetExclusive) { + const size_t midOffset = (lowOffsetInclusive + highOffsetExclusive) / 2; + const char *chars = &bucket.chars[bucket.spacing * midOffset]; + + bool isMidGreater = false; + bool isMidLess = false; + for (size_t i = 0; i < strLength; i++) { + if (chars[i] > strChars[i]) { + isMidGreater = true; + break; + } else if (chars[i] < strChars[i]) { + isMidLess = true; + break; + } + } + + if (isMidLess) + lowOffsetInclusive = midOffset + 1; + else if (isMidGreater) + highOffsetExclusive = midOffset; + else { + _index = bucket.wordIndexes[midOffset] + 1; + break; + } + } +} + +MiniscriptInstructionOutcome DictionaryModifier::scriptSetString(MiniscriptThread *thread, const DynamicValue &value) { + if (value.getType() != DynamicValueTypes::kString) { + thread->error("Tried to set dictionary string to something that wasn't a string"); + return kMiniscriptInstructionOutcomeFailed; + } + + if (_str != value.getString()) { + _str = value.getString(); + _isIndexResolved = false; + } + + return kMiniscriptInstructionOutcomeContinue; +} + +MiniscriptInstructionOutcome DictionaryModifier::scriptSetIndex(MiniscriptThread *thread, const DynamicValue &value) { + int32 asInteger = 0; + if (!value.roundToInt(asInteger)) { + thread->error("Tried to set dictionary index to something that wasn't a number"); + return kMiniscriptInstructionOutcomeFailed; + } + + _index = asInteger; + if (_index < 1) + _str.clear(); + else { + const size_t indexAdjusted = _index - 1; + const Common::Array &sortedWords = _plugIn->getWordGameData()->getSortedWords(); + if (indexAdjusted >= sortedWords.size()) + _str.clear(); + else + _str = Common::String(sortedWords[indexAdjusted].chars, sortedWords[indexAdjusted].length); + } + + _isIndexResolved = true; + + return kMiniscriptInstructionOutcomeContinue; +} + +Common::SharedPtr DictionaryModifier::shallowClone() const { + return Common::SharedPtr(new DictionaryModifier(*this)); +} + +WordMixerModifier::WordMixerModifier() { +} + +bool WordMixerModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data) { + return true; +} + +Common::SharedPtr WordMixerModifier::shallowClone() const { + return Common::SharedPtr(new WordMixerModifier(*this)); +} + +ObsidianPlugIn::ObsidianPlugIn(const Common::SharedPtr &wgData) + : _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this), + _dictionaryModifierFactory(this), _wordMixerModifierFactory(this), _wgData(wgData) { } void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const { registrar->registerPlugInModifier("Movement", &_movementModifierFactory); registrar->registerPlugInModifier("rectshift", &_rectShiftModifierFactory); registrar->registerPlugInModifier("TextWork", &_textWorkModifierFactory); + registrar->registerPlugInModifier("Dictionary", &_dictionaryModifierFactory); + registrar->registerPlugInModifier("WordMixer", &_wordMixerModifierFactory); +} + +const Common::SharedPtr& ObsidianPlugIn::getWordGameData() const { + return _wgData; +} + +bool WordGameData::load(Common::SeekableReadStream *stream, const WordGameLoadBucket *buckets, uint numBuckets, uint alignment, bool backwards) { + _buckets.resize(numBuckets); + + size_t totalWords = 0; + for (size_t i = 0; i < numBuckets; i++) { + const WordGameLoadBucket &inBucket = buckets[i]; + WordBucket &outBucket = _buckets[i]; + + uint32 sizeBytes = inBucket.endAddress - inBucket.startAddress; + uint wordLength = i; + uint spacing = (wordLength + alignment) - (wordLength % alignment); + + outBucket.spacing = spacing; + outBucket.chars.resize(sizeBytes); + + assert(sizeBytes % alignment == 0); + + if (sizeBytes > 0) { + if (!stream->seek(inBucket.startAddress, SEEK_SET)) + return false; + stream->read(&outBucket.chars[0], sizeBytes); + } + + uint numWords = sizeBytes / spacing; + outBucket.wordIndexes.resize(numWords); + + if (backwards) { + for (size_t wordIndex = 0; wordIndex < numWords / 2; wordIndex++) { + char *swapA = &outBucket.chars[wordIndex * spacing]; + char *swapB = &outBucket.chars[(numWords - 1 - wordIndex) * spacing]; + + for (size_t chIndex = 0; chIndex < wordLength; chIndex++) { + char temp = swapA[chIndex]; + swapA[chIndex] = swapB[chIndex]; + swapB[chIndex] = temp; + } + } + } + + totalWords += numWords; + } + + _sortedWords.resize(totalWords); + + Common::Array currentWordIndexes; + currentWordIndexes.resize(numBuckets); + + for (size_t i = 0; i < numBuckets; i++) + currentWordIndexes[i] = 0; + + for (size_t wordIndex = 0; wordIndex < totalWords; wordIndex++) { + size_t bestBucket = numBuckets; + const char *bestChars = nullptr; + for (size_t bucketIndex = 0; bucketIndex < numBuckets; bucketIndex++) { + size_t wordOffset = currentWordIndexes[bucketIndex] * _buckets[bucketIndex].spacing; + if (wordOffset < _buckets[bucketIndex].chars.size()) { + const char *candidate = &_buckets[bucketIndex].chars[wordOffset]; + bool isWorse = true; + if (bestChars == nullptr) + isWorse = false; + else { + // The best bucket will always be shorter if it's set, so this is only better if it precedes it alphabetically + for (size_t i = 0; i < bestBucket; i++) { + if (candidate[i] > bestChars[i]) { + break; + } else if (candidate[i] < bestChars[i]) { + isWorse = false; + break; + } + } + } + + if (!isWorse) { + bestBucket = bucketIndex; + bestChars = candidate; + } + } + } + + assert(bestChars != nullptr); + + const size_t bucketWordIndex = currentWordIndexes[bestBucket]; + _buckets[bestBucket].wordIndexes[bucketWordIndex] = wordIndex; + currentWordIndexes[bestBucket]++; + + _sortedWords[wordIndex].chars = bestChars; + _sortedWords[wordIndex].length = bestBucket; + } + + return !stream->err(); +} + +const Common::Array &WordGameData::getWordBuckets() const { + return _buckets; +} + +const Common::Array& WordGameData::getSortedWords() const { + return _sortedWords; } } // End of namespace ObsidianPlugIn namespace PlugIns { -Common::SharedPtr createObsidian() { - return Common::SharedPtr(new Obsidian::ObsidianPlugIn()); +Common::SharedPtr createObsidian(const Common::SharedPtr &wgData) { + return Common::SharedPtr(new Obsidian::ObsidianPlugIn(wgData)); } } // End of namespace PlugIns diff --git a/engines/mtropolis/plugin/obsidian.h b/engines/mtropolis/plugin/obsidian.h index f7ce43d3768..f1397961eab 100644 --- a/engines/mtropolis/plugin/obsidian.h +++ b/engines/mtropolis/plugin/obsidian.h @@ -31,6 +31,9 @@ namespace MTropolis { namespace Obsidian { +class ObsidianPlugIn; +class WordGameData; + class MovementModifier : public Modifier { public: bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data); @@ -79,16 +82,91 @@ private: int32 _lastChar; }; +class DictionaryModifier : public Modifier { +public: + DictionaryModifier(); + + bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::DictionaryModifier &data); + + bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib); + MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib); + +#ifdef MTROPOLIS_DEBUG_ENABLE + const char *debugGetTypeName() const override { return "Dictionary Modifier"; } +#endif + +private: + void resolveStringIndex(); + MiniscriptInstructionOutcome scriptSetString(MiniscriptThread *thread, const DynamicValue &value); + MiniscriptInstructionOutcome scriptSetIndex(MiniscriptThread *thread, const DynamicValue &value); + + Common::SharedPtr shallowClone() const override; + + Common::String _str; + + const ObsidianPlugIn *_plugIn; + int32 _index; + bool _isIndexResolved; +}; + +class WordMixerModifier : public Modifier { +public: + WordMixerModifier(); + + bool load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data); + +#ifdef MTROPOLIS_DEBUG_ENABLE + const char *debugGetTypeName() const override { return "WordMixer Modifier"; } +#endif + +private: + Common::SharedPtr shallowClone() const override; +}; + class ObsidianPlugIn : public MTropolis::PlugIn { public: - ObsidianPlugIn(); + ObsidianPlugIn(const Common::SharedPtr &wgData); void registerModifiers(IPlugInModifierRegistrar *registrar) const override; + const Common::SharedPtr &getWordGameData() const; + private: PlugInModifierFactory _movementModifierFactory; PlugInModifierFactory _rectShiftModifierFactory; PlugInModifierFactory _textWorkModifierFactory; + PlugInModifierFactory _wordMixerModifierFactory; + PlugInModifierFactory _dictionaryModifierFactory; + + Common::SharedPtr _wgData; +}; + +struct WordGameLoadBucket { + uint32 startAddress; + uint32 endAddress; +}; + +class WordGameData { +public: + struct WordBucket { + Common::Array chars; + Common::Array wordIndexes; + uint32 spacing; + }; + + struct SortedWord { + const char *chars; + uint length; + }; + + bool load(Common::SeekableReadStream *stream, const WordGameLoadBucket *buckets, uint numBuckets, uint alignment, bool backwards); + + const Common::Array &getWordBuckets() const; + const Common::Array &getSortedWords() const; + +private: + Common::Array _buckets; + Common::Array _sortedWords; }; } // End of namespace Obsidian diff --git a/engines/mtropolis/plugin/obsidian_data.cpp b/engines/mtropolis/plugin/obsidian_data.cpp index 7aec2579487..3608c44ae57 100644 --- a/engines/mtropolis/plugin/obsidian_data.cpp +++ b/engines/mtropolis/plugin/obsidian_data.cpp @@ -65,6 +65,23 @@ DataReadErrorCode TextWorkModifier::load(PlugIn& plugIn, const PlugInModifier& p return kDataReadErrorNone; } +DataReadErrorCode WordMixerModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) { + if (prefix.plugInRevision != 1) + return kDataReadErrorUnsupportedRevision; + + return kDataReadErrorNone; +} + +DataReadErrorCode DictionaryModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) { + if (prefix.plugInRevision != 1) + return kDataReadErrorUnsupportedRevision; + + if (!str.load(reader) || !index.load(reader)) + return kDataReadErrorReadFailed; + + return kDataReadErrorNone; +} + } // End of namespace Obsidian } // End of namespace Data diff --git a/engines/mtropolis/plugin/obsidian_data.h b/engines/mtropolis/plugin/obsidian_data.h index 2fedd316d23..230fb8254af 100644 --- a/engines/mtropolis/plugin/obsidian_data.h +++ b/engines/mtropolis/plugin/obsidian_data.h @@ -71,6 +71,19 @@ protected: DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override; }; +struct DictionaryModifier : public PlugInModifierData { + PlugInTypeTaggedValue str; + PlugInTypeTaggedValue index; + +protected: + DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override; +}; + +struct WordMixerModifier : public PlugInModifierData { +protected: + DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override; +}; + } // End of namespace Obsidian } // End of namespace Data diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp index 473676958aa..2e947a39a5f 100644 --- a/engines/mtropolis/plugin/standard.cpp +++ b/engines/mtropolis/plugin/standard.cpp @@ -812,6 +812,12 @@ VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::Shared } } } + if (_terminateWhen.respondsTo(msg->getEvent())) { +#ifdef MTROPOLIS_DEBUG_ENABLE + if (Debugger *debugger = runtime->debugGetDebugger()) + debugger->notify(kDebugSeverityWarning, "MIDI player ordered to terminate, which isn't supported yet"); +#endif + } return kVThreadReturn; } diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h index 57889f91fc5..8f86b3ce5c9 100644 --- a/engines/mtropolis/plugin/standard.h +++ b/engines/mtropolis/plugin/standard.h @@ -50,6 +50,7 @@ public: #ifdef MTROPOLIS_DEBUG_ENABLE const char *debugGetTypeName() const override { return "Cursor Modifier"; } + SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; } #endif private: @@ -183,7 +184,7 @@ public: #ifdef MTROPOLIS_DEBUG_ENABLE const char *debugGetTypeName() const override { return "MIDI Modifier"; } - SupportStatus debugGetSupportStatus() const override { return kSupportStatusPartial; } + SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; } #endif private: diff --git a/engines/mtropolis/plugins.h b/engines/mtropolis/plugins.h index 3c8ab00f903..107d05e74c7 100644 --- a/engines/mtropolis/plugins.h +++ b/engines/mtropolis/plugins.h @@ -28,12 +28,18 @@ class MidiDriver; namespace MTropolis { +namespace Obsidian { + +class WordGameData; + +} // End of namespace Obsidian + class PlugIn; namespace PlugIns { Common::SharedPtr createStandard(); -Common::SharedPtr createObsidian(); +Common::SharedPtr createObsidian(const Common::SharedPtr &wgData); } // End of namespace PlugIns diff --git a/engines/mtropolis/runtime.cpp b/engines/mtropolis/runtime.cpp index a538652929b..5a7fdd67d9e 100644 --- a/engines/mtropolis/runtime.cpp +++ b/engines/mtropolis/runtime.cpp @@ -2136,7 +2136,10 @@ MiniscriptInstructionOutcome WorldManagerInterface::writeRefAttribute(Miniscript DynamicValueWriteFuncHelper::create(this, result); return kMiniscriptInstructionOutcomeContinue; } - + if (attrib == "refreshcursor") { + DynamicValueWriteFuncHelper::create(this, result); + return kMiniscriptInstructionOutcomeContinue; + } return RuntimeObject::writeRefAttribute(thread, result, attrib); } @@ -2167,6 +2170,16 @@ MiniscriptInstructionOutcome WorldManagerInterface::setCurrentScene(MiniscriptTh return kMiniscriptInstructionOutcomeContinue; } +MiniscriptInstructionOutcome WorldManagerInterface::setRefreshCursor(MiniscriptThread *thread, const DynamicValue &value) { + if (value.getType() != DynamicValueTypes::kBoolean) + return kMiniscriptInstructionOutcomeFailed; + + if (value.getBool()) + thread->getRuntime()->forceCursorRefreshOnce(); + + return kMiniscriptInstructionOutcomeContinue; +} + SystemInterface::SystemInterface() : _masterVolume(kFullVolume) { } @@ -3206,7 +3219,8 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, ISaveUIProvider *saveProv _nextRuntimeGUID(1), _realDisplayMode(kColorDepthModeInvalid), _fakeDisplayMode(kColorDepthModeInvalid), _displayWidth(1024), _displayHeight(768), _realTimeBase(0), _playTimeBase(0), _sceneTransitionState(kSceneTransitionStateNotTransitioning), _lastFrameCursor(nullptr), _defaultCursor(new DefaultCursorGraphic()), _platform(kProjectPlatformUnknown), - _cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false), _haveModifierOverrideCursor(false) { + _cachedMousePosition(Point16::create(0, 0)), _realMousePosition(Point16::create(0, 0)), _trackedMouseOutside(false), + _forceCursorRefreshOnce(true), _haveModifierOverrideCursor(false) { _random.reset(new Common::RandomSource("mtropolis")); _vthread.reset(new VThread()); @@ -3301,6 +3315,13 @@ bool Runtime::runFrame() { continue; } + if (_forceCursorRefreshOnce) { + _forceCursorRefreshOnce = false; + UpdateMousePositionTaskData *taskData = _vthread->pushTask("Runtime::updateMousePositionTask", this, &Runtime::updateMousePositionTask); + taskData->x = _cachedMousePosition.x; + taskData->y = _cachedMousePosition.y; + } + if (_queuedProjectDesc) { Common::SharedPtr desc = _queuedProjectDesc; _queuedProjectDesc.reset(); @@ -4573,6 +4594,10 @@ void Runtime::clearModifierCursorOverride() { } } +void Runtime::forceCursorRefreshOnce() { + _forceCursorRefreshOnce = true; +} + Common::RandomSource *Runtime::getRandom() const { return _random.get(); } diff --git a/engines/mtropolis/runtime.h b/engines/mtropolis/runtime.h index ab181738fb7..c8bc1a405f1 100644 --- a/engines/mtropolis/runtime.h +++ b/engines/mtropolis/runtime.h @@ -1489,6 +1489,7 @@ public: const Point16 &getCachedMousePosition() const; void setModifierCursorOverride(uint32 cursorID); void clearModifierCursorOverride(); + void forceCursorRefreshOnce(); Common::RandomSource *getRandom() const; WorldManagerInterface *getWorldManagerInterface() const; @@ -1670,6 +1671,7 @@ private: Common::WeakPtr _mouseOverObject; Common::WeakPtr _mouseTrackingObject; bool _trackedMouseOutside; + bool _forceCursorRefreshOnce; uint32 _modifierOverrideCursorID; bool _haveModifierOverrideCursor; @@ -1768,6 +1770,7 @@ public: private: MiniscriptInstructionOutcome setCurrentScene(MiniscriptThread *thread, const DynamicValue &value); + MiniscriptInstructionOutcome setRefreshCursor(MiniscriptThread *thread, const DynamicValue &value); }; class AssetManagerInterface : public RuntimeObject {