/* 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 "graphics/managed_surface.h" #include "mtropolis/plugin/obsidian.h" #include "mtropolis/plugins.h" #include "mtropolis/miniscript.h" namespace MTropolis { namespace Obsidian { MovementModifier::MovementModifier() : _type(false), _rate(0), _frequency(0), _moveStartTime(0), _runtime(nullptr) { } MovementModifier::~MovementModifier() { if (_moveEvent) _moveEvent->cancel(); } bool MovementModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data) { if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_enableWhen.load(data.enableWhen.value.asEvent)) return false; if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_disableWhen.load(data.disableWhen.value.asEvent)) return false; if (data.rate.type != Data::PlugInTypeTaggedValue::kFloat) return false; _rate = data.rate.value.asFloat.toXPFloat().toDouble(); if (data.frequency.type != Data::PlugInTypeTaggedValue::kInteger) return false; _frequency = data.frequency.value.asInt; if (data.type.type != Data::PlugInTypeTaggedValue::kBoolean) return false; _type = (data.type.value.asBoolean != 0); if (data.dest.type != Data::PlugInTypeTaggedValue::kPoint || !data.dest.value.asPoint.toScummVMPoint(_dest)) return false; if (data.triggerEvent.type != Data::PlugInTypeTaggedValue::kEvent || !_triggerEvent.load(data.triggerEvent.value.asEvent)) return false; return true; } bool MovementModifier::respondsToEvent(const Event &evt) const { return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt); } VThreadState MovementModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr &msg) { if (_enableWhen.respondsTo(msg->getEvent())) { Structural *structural = findStructuralOwner(); if (structural == nullptr || !structural->isElement() || !static_cast(structural)->isVisual()) { warning("Movement modifier wasn't attached to a visual element"); return kVThreadError; } VisualElement *visual = static_cast(structural); Common::Rect startRect = visual->getRelativeRect(); _moveStartPoint = Common::Point(startRect.left, startRect.top); _moveStartTime = runtime->getPlayTime(); if (!_moveEvent) { _runtime = runtime; _moveEvent = runtime->getScheduler().scheduleMethod(runtime->getPlayTime() + 1, this); } } if (_disableWhen.respondsTo(msg->getEvent())) { disable(runtime); } return kVThreadReturn; } void MovementModifier::disable(Runtime *runtime) { if (_moveEvent) { _moveEvent->cancel(); _moveEvent.reset(); } } MiniscriptInstructionOutcome MovementModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { if (attrib == "type") { DynamicValueWriteBoolHelper::create(&_type, result); return kMiniscriptInstructionOutcomeContinue; } if (attrib == "dest") { DynamicValueWritePointHelper::create(&_dest, result); return kMiniscriptInstructionOutcomeContinue; } if (attrib == "rate") { DynamicValueWriteFloatHelper::create(&_rate, result); return kMiniscriptInstructionOutcomeContinue; } if (attrib == "frequency") { DynamicValueWriteIntegerHelper::create(&_frequency, result); return kMiniscriptInstructionOutcomeContinue; } return Modifier::writeRefAttribute(thread, result, attrib); } #ifdef MTROPOLIS_DEBUG_ENABLE void MovementModifier::debugInspect(IDebugInspectionReport *report) const { Modifier::debugInspect(report); report->declareDynamic("enableWhen", Common::String::format("Event(%i,%i)", static_cast(_enableWhen.eventType), static_cast(_enableWhen.eventInfo))); report->declareDynamic("disableWhen", Common::String::format("Event(%i,%i)", static_cast(_disableWhen.eventType), static_cast(_disableWhen.eventInfo))); report->declareDynamic("rate", Common::String::format("%g", _rate)); report->declareDynamic("frequency", Common::String::format("%i", static_cast(_frequency))); report->declareDynamic("type", Common::String::format(_type ? "true" : "false")); report->declareDynamic("dest", Common::String::format("(%i,%i)", static_cast(_dest.x), static_cast(_dest.y))); report->declareDynamic("triggerEvent", Common::String::format("Event(%i,%i)", static_cast(_triggerEvent.eventType), static_cast(_triggerEvent.eventInfo))); } #endif Common::SharedPtr MovementModifier::shallowClone() const { return Common::SharedPtr(new MovementModifier(*this)); } const char *MovementModifier::getDefaultName() const { return "Movement"; } void MovementModifier::triggerMove(Runtime *runtime) { _moveEvent.reset(); Structural *structural = findStructuralOwner(); if (structural == nullptr || !structural->isElement() || !static_cast(structural)->isVisual()) { warning("Movement modifier wasn't attached to a visual element"); return; } VisualElement *visual = static_cast(structural); Common::Point delta = _dest - _moveStartPoint; double deltaLength = sqrt(delta.x * delta.x + delta.y * delta.y); double progression = 1.0; if (deltaLength > 0.0 && _rate > 0.0) { double distance = static_cast(runtime->getPlayTime() - _moveStartTime) * _rate / 1000.0; progression = distance / deltaLength; if (progression > 1.0) progression = 1.0; if (progression < 0.0) progression = 0.0; } int32 targetX = _moveStartPoint.x + static_cast(round((_dest.x - _moveStartPoint.x) * progression)); int32 targetY = _moveStartPoint.y + static_cast(round((_dest.y - _moveStartPoint.y) * progression)); Common::Rect relRect = visual->getRelativeRect(); int32 xDelta = targetX - relRect.left; int32 yDelta = targetY - relRect.top; relRect.left += xDelta; relRect.right += xDelta; relRect.top += yDelta; relRect.bottom += yDelta; visual->setRelativeRect(relRect); if (progression == 1.0) { Common::SharedPtr props(new MessageProperties(_triggerEvent, DynamicValue(), visual->getSelfReference())); Common::SharedPtr dispatch(new MessageDispatch(props, visual, true, true, false)); runtime->sendMessageOnVThread(dispatch); } else { _moveEvent = runtime->getScheduler().scheduleMethod(runtime->getPlayTime() + 1, this); } } RectShiftModifier::RectShiftModifier() : _direction(0), _runtime(nullptr), _isActive(false) { } RectShiftModifier::~RectShiftModifier() { if (_isActive) _runtime->removePostEffect(this); } bool RectShiftModifier::respondsToEvent(const Event &evt) const { return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt); } VThreadState RectShiftModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr &msg) { if (_enableWhen.respondsTo(msg->getEvent()) && !_isActive) { _runtime = runtime; _runtime->addPostEffect(this); _isActive = true; } if (_disableWhen.respondsTo(msg->getEvent()) && _isActive) { disable(runtime); } return kVThreadReturn; } void RectShiftModifier::disable(Runtime *runtime) { if (_isActive) { _isActive = false; _runtime->removePostEffect(this); _runtime = nullptr; } } bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data) { if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_enableWhen.load(data.enableWhen.value.asEvent)) return false; if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_disableWhen.load(data.disableWhen.value.asEvent)) return false; if (data.direction.type != Data::PlugInTypeTaggedValue::kInteger) return false; _direction = data.direction.value.asInt; if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_enableWhen.load(data.enableWhen.value.asEvent)) return false; return true; } MiniscriptInstructionOutcome RectShiftModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { if (attrib == "direction") { DynamicValueWriteIntegerHelper::create(&_direction, result); return kMiniscriptInstructionOutcomeContinue; } return Modifier::writeRefAttribute(thread, result, attrib); } void RectShiftModifier::renderPostEffect(Graphics::ManagedSurface &surface) const { Structural *structural = findStructuralOwner(); if (!structural) return; if (!structural->isElement() || !static_cast(structural)->isVisual()) return; VisualElement *visual = static_cast(structural); Common::Point absOrigin = visual->getCachedAbsoluteOrigin(); Common::Rect relRect = visual->getRelativeRect(); Common::Rect absRect(absOrigin.x, absOrigin.y, absOrigin.x + relRect.width(), absOrigin.y + relRect.height()); if (absRect.left < 0) absRect.left = 0; if (absRect.right >= surface.w) absRect.right = surface.w; if (absRect.top < 0) absRect.top = 0; if (absRect.bottom >= surface.h) absRect.bottom = surface.h; if (_direction == 1) { if (absRect.bottom + 1 >= surface.h) absRect.bottom--; } else if (_direction == 4) { if (absRect.right + 1 >= surface.w) absRect.right--; } else return; if (!absRect.isValidRect()) return; uint pitch = (absRect.right - absRect.left) * surface.format.bytesPerPixel; for (int32 y = absRect.top; y < absRect.bottom; y++) { void *destPixels = surface.getBasePtr(absRect.left, y); void *srcPixels = destPixels; if (_direction == 1) srcPixels = surface.getBasePtr(absRect.left, y + 1); else if (_direction == 4) srcPixels = surface.getBasePtr(absRect.left + 1, y); memmove(destPixels, srcPixels, pitch); } } #ifdef MTROPOLIS_DEBUG_ENABLE void RectShiftModifier::debugInspect(IDebugInspectionReport *report) const { Modifier::debugInspect(report); report->declareDynamic("direction", Common::String::format("%i", static_cast(_direction))); } #endif Common::SharedPtr RectShiftModifier::shallowClone() const { Common::SharedPtr clone(new RectShiftModifier(*this)); clone->_isActive = false; clone->_runtime = nullptr; return clone; } const char *RectShiftModifier::getDefaultName() const { return "RectShift"; } TextWorkModifier::TextWorkModifier() : _firstChar(0), _lastChar(0) { } bool TextWorkModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data) { return true; } bool TextWorkModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) { if (attrib == "numchar") { result.setInt(_string.size()); return true; } else if (attrib == "output") { int32 firstChar = _firstChar - 1; int32 len = _lastChar - _firstChar + 1; if (_firstChar < 0) { len += firstChar; firstChar = 0; } if (len <= 0 || static_cast(firstChar) >= _string.size()) result.setString(""); else { const size_t availChars = _string.size() - firstChar; if (static_cast(len) > availChars) len = availChars; result.setString(_string.substr(firstChar, len)); } return true; } else if (attrib == "exists") { bool exists = (caseInsensitiveFind(_string, _token) != Common::String::npos); result.setInt(exists ? 1 : 0); return true; } else if (attrib == "index") { size_t index = caseInsensitiveFind(_string, _token); if (index == Common::String::npos) index = 0; else index++; result.setInt(index); return true; } else if (attrib == "numword") { int numWords = 0; bool lastWasWhitespace = true; for (size_t i = 0; i < _string.size(); i++) { char c = _string[i]; bool isWhitespace = (c <= ' '); if (lastWasWhitespace && !isWhitespace) numWords++; lastWasWhitespace = isWhitespace; } result.setInt(numWords); return true; } return Modifier::readAttribute(thread, result, attrib); } MiniscriptInstructionOutcome TextWorkModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { if (attrib == "string") { DynamicValueWriteStringHelper::create(&_string, result); return kMiniscriptInstructionOutcomeContinue; } else if (attrib == "firstchar") { DynamicValueWriteIntegerHelper::create(&_firstChar, result); return kMiniscriptInstructionOutcomeContinue; } else if (attrib == "lastchar") { DynamicValueWriteIntegerHelper::create(&_lastChar, result); return kMiniscriptInstructionOutcomeContinue; } else if (attrib == "token") { DynamicValueWriteStringHelper::create(&_token, result); return kMiniscriptInstructionOutcomeContinue; } else if (attrib == "firstword") { DynamicValueWriteFuncHelper::create(this, result); return kMiniscriptInstructionOutcomeContinue; } else if (attrib == "lastword") { DynamicValueWriteFuncHelper::create(this, result); return kMiniscriptInstructionOutcomeContinue; } return Modifier::writeRefAttribute(thread, result, attrib); } Common::SharedPtr TextWorkModifier::shallowClone() const { return Common::SharedPtr(new TextWorkModifier(*this)); } const char *TextWorkModifier::getDefaultName() const { return "TextWork"; } MiniscriptInstructionOutcome TextWorkModifier::scriptSetFirstWord(MiniscriptThread *thread, const DynamicValue &value) { // This and lastword are only used in tandem with lastword, exact functionality is unclear since it's // also used in tandem with "output" which is normally used with firstchar+lastchar. // We attempt to emulate it by setting firstchar+lastchar to the correct values int32 asInteger = 0; if (!value.roundToInt(asInteger)) return kMiniscriptInstructionOutcomeFailed; int numWords = 0; bool lastWasWhitespace = true; for (size_t i = 0; i < _string.size(); i++) { char c = _string[i]; bool isWhitespace = (c <= ' '); if (lastWasWhitespace && !isWhitespace) { numWords++; if (numWords == asInteger) { _firstChar = i + 1; return kMiniscriptInstructionOutcomeContinue; } } lastWasWhitespace = isWhitespace; } thread->error("Invalid index for 'firstword'"); return kMiniscriptInstructionOutcomeFailed; } MiniscriptInstructionOutcome TextWorkModifier::scriptSetLastWord(MiniscriptThread *thread, const DynamicValue &value) { int32 asInteger = 0; if (!value.roundToInt(asInteger)) return kMiniscriptInstructionOutcomeFailed; int numWordEnds = 0; bool lastWasWhitespace = true; for (size_t i = 0; i < _string.size(); i++) { char c = _string[i]; bool isWhitespace = (c <= ' '); if (!lastWasWhitespace && isWhitespace) { numWordEnds++; if (numWordEnds == asInteger) { _firstChar = i - 1; return kMiniscriptInstructionOutcomeContinue; } } lastWasWhitespace = isWhitespace; if (numWordEnds == asInteger) { _lastChar = i; return kMiniscriptInstructionOutcomeContinue; } } if (!lastWasWhitespace) { numWordEnds++; if (numWordEnds == asInteger) { _lastChar = _string.size(); return kMiniscriptInstructionOutcomeContinue; } } thread->error("Invalid index for 'firstword'"); return kMiniscriptInstructionOutcomeFailed; } DictionaryModifier::DictionaryModifier() : _plugIn(nullptr), _isIndexResolved(false), _index(0) { } bool DictionaryModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::DictionaryModifier &data) { if (data.str.type != Data::PlugInTypeTaggedValue::kString) return false; _str = data.str.value.asString; 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 = static_cast(_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)); } const char *DictionaryModifier::getDefaultName() const { return "Dictionary"; } WordMixerModifier::WordMixerModifier() : _matches(0), _result(0), _plugIn(nullptr) { } bool WordMixerModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data) { _plugIn = static_cast(context.plugIn); return true; } bool WordMixerModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) { if (attrib == "result") { result.setInt(_result); return true; } if (attrib == "matches") { result.setInt(_matches); return true; } if (attrib == "output") { result.setString(_output); return true; } return Modifier::readAttribute(thread, result, attrib); } MiniscriptInstructionOutcome WordMixerModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { if (attrib == "input") { DynamicValueWriteFuncHelper::create(this, result); return kMiniscriptInstructionOutcomeContinue; } if (attrib == "search") { DynamicValueWriteFuncHelper::create(this, result); return kMiniscriptInstructionOutcomeContinue; } return Modifier::writeRefAttribute(thread, result, attrib); } MiniscriptInstructionOutcome WordMixerModifier::scriptSetInput(MiniscriptThread *thread, const DynamicValue &value) { if (value.getType() != DynamicValueTypes::kString) { thread->error("Invalid type for WordMixer input attribute"); return kMiniscriptInstructionOutcomeFailed; } _input = value.getString(); Common::Array sourceChars; for (char c : _input) { if (c > ' ') sourceChars.push_back(invariantToLower(c)); } Common::Array charIsUsed; charIsUsed.resize(sourceChars.size()); const Common::Array &wordBuckets = _plugIn->getWordGameData()->getWordBuckets(); _output.clear(); _matches = 0; size_t numWordBuckets = wordBuckets.size(); for (size_t rbucket = 0; rbucket < numWordBuckets; rbucket++) { size_t wordLength = numWordBuckets - 1 - rbucket; const WordGameData::WordBucket &bucket = wordBuckets[wordLength]; size_t numWords = bucket._wordIndexes.size(); for (size_t wi = 0; wi < numWords; wi++) { const char *wordChars = &bucket._chars[bucket._spacing * wi]; for (bool &b : charIsUsed) b = false; bool isMatch = true; for (size_t ci = 0; ci < wordLength; ci++) { const char wordChar = wordChars[ci]; bool foundAvailableSource = false; for (size_t srci = 0; srci < sourceChars.size(); srci++) { if (sourceChars[srci] == wordChar && !charIsUsed[srci]) { foundAvailableSource = true; charIsUsed[srci] = true; break; } } if (!foundAvailableSource) { isMatch = false; break; } } if (isMatch) { if (_matches > 0) _output += ' '; _output += Common::String(wordChars, wordLength); _matches++; } } if (_matches > 0) break; } if (_matches == 0) _output = "xxx"; return kMiniscriptInstructionOutcomeContinue; } MiniscriptInstructionOutcome WordMixerModifier::scriptSetSearch(MiniscriptThread *thread, const DynamicValue &value) { if (value.getType() != DynamicValueTypes::kBoolean) { thread->error("Invalid type for WordMixer search attribute"); return kMiniscriptInstructionOutcomeFailed; } if (!value.getBool()) return kMiniscriptInstructionOutcomeContinue; size_t searchLength = _input.size(); const Common::Array &buckets = _plugIn->getWordGameData()->getWordBuckets(); _result = 0; if (searchLength < buckets.size()) { const WordGameData::WordBucket &bucket = buckets[searchLength]; for (size_t wi = 0; wi < bucket._wordIndexes.size(); wi++) { const char *wordChars = &bucket._chars[wi * bucket._spacing]; bool isMatch = true; for (size_t ci = 0; ci < searchLength; ci++) { if (invariantToLower(_input[ci]) != wordChars[ci]) { isMatch = false; break; } } if (isMatch) { _result = 1; break; } } } return kMiniscriptInstructionOutcomeContinue; } Common::SharedPtr WordMixerModifier::shallowClone() const { return Common::SharedPtr(new WordMixerModifier(*this)); } const char *WordMixerModifier::getDefaultName() const { return "WordMixer"; } XorModModifier::XorModModifier() : _shapeID(0) { } bool XorModModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorModModifier &data) { if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent) return false; if (!_enableWhen.load(data.enableWhen.value.asEvent)) return false; if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent) return false; if (!_disableWhen.load(data.disableWhen.value.asEvent)) return false; if (data.shapeID.type != Data::PlugInTypeTaggedValue::kInteger) return false; _shapeID = data.shapeID.value.asInt; return true; } bool XorModModifier::respondsToEvent(const Event &evt) const { return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt); } VThreadState XorModModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr &msg) { if (_enableWhen.respondsTo(msg->getEvent())) { Structural *owner = findStructuralOwner(); if (!owner) return kVThreadError; if (!owner->isElement()) return kVThreadReturn; Element *element = static_cast(owner); if (!element->isVisual()) return kVThreadReturn; VisualElement *visual = static_cast(element); VisualElementRenderProperties renderProps = visual->getRenderProperties(); renderProps.setInkMode(VisualElementRenderProperties::kInkModeXor); if (_shapeID == 0) renderProps.setShape(VisualElementRenderProperties::kShapeRect); else renderProps.setShape(static_cast(VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 + _shapeID - 1)); visual->setRenderProperties(renderProps, Common::WeakPtr()); return kVThreadReturn; } if (_disableWhen.respondsTo(msg->getEvent())) { disable(runtime); return kVThreadReturn; } return kVThreadReturn; } void XorModModifier::disable(Runtime *runtime) { // This is a special-purpose modifier and is never disabled } Common::SharedPtr XorModModifier::shallowClone() const { return Common::SharedPtr(new XorModModifier(*this)); } const char *XorModModifier::getDefaultName() const { return "XorMod"; } XorCheckModifier::XorCheckModifier() : _allClear(false) { } bool XorCheckModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorCheckModifier &data) { return true; } bool XorCheckModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) { if (attrib == "allclear") { result.setBool(_allClear); return true; } return Modifier::readAttribute(thread, result, attrib); } MiniscriptInstructionOutcome XorCheckModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) { if (attrib == "checknow") { DynamicValueWriteFuncHelper::create(this, result); return kMiniscriptInstructionOutcomeContinue; } return Modifier::writeRefAttribute(thread, result, attrib); } Common::SharedPtr XorCheckModifier::shallowClone() const { return Common::SharedPtr(new XorCheckModifier(*this)); } const char *XorCheckModifier::getDefaultName() const { return "XorCheck"; } MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value) { if (value.getType() != DynamicValueTypes::kBoolean) return kMiniscriptInstructionOutcomeFailed; if (!value.getBool()) return kMiniscriptInstructionOutcomeContinue; Structural *scene = findStructuralOwner(); while (!scene->getParent()->isSubsection()) scene = scene->getParent(); Common::Array xorElements; recursiveFindXorElements(scene, xorElements); Common::Rect triRects[4]; for (int i = 0; i < 4; i++) triRects[i] = Common::Rect(0, 0, 0, 0); Common::Array pendingRects; for (VisualElement *element : xorElements) { VisualElementRenderProperties::Shape shape = element->getRenderProperties().getShape(); Common::Rect rect = element->getRelativeRect(); Common::Point absOrigin = element->getCachedAbsoluteOrigin(); Common::Rect absRect = rect; absRect.translate(absOrigin.x - rect.left, absOrigin.y - rect.top); if (shape >= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 && shape <= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri4) triRects[shape - VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1] = absRect; else pendingRects.push_back(absRect); } // The canvas puzzle has 4 triangles, right-angled in each corner, pairs 1-4 and 2-3 form rects. // It isn't possible to solve the puzzle unless both rects are formed. So, we do this by forming the rects and // then eliminating overlaps. If the rects can't be formed, the puzzle fails. if (triRects[0] == triRects[3]) pendingRects.push_back(triRects[0]); else { _allClear = false; return kMiniscriptInstructionOutcomeContinue; } if (triRects[1] == triRects[2]) pendingRects.push_back(triRects[1]); else { _allClear = false; return kMiniscriptInstructionOutcomeContinue; } Common::Array maskedRects; while (pendingRects.size() > 0) { const Common::Rect pendingRect = pendingRects.back(); pendingRects.pop_back(); bool hasIntersection = false; size_t intersectionIndex = 0; for (size_t j = 0; j < maskedRects.size(); j++) { if (maskedRects[j].intersects(pendingRect)) { hasIntersection = true; intersectionIndex = j; } } if (!hasIntersection) { maskedRects.push_back(pendingRect); continue; } if (pendingRect == maskedRects[intersectionIndex]) { // Total overlap maskedRects.remove_at(intersectionIndex); continue; } const Common::Rect intersectingRect = maskedRects[intersectionIndex]; // Try to subdivide the intersecting rect using one of the axes of the incoming rect. // If this succeeds, requeue the intersecting rect fragments and add the pending rect // to the workspace. Since that amounts to replacement, just replace the rect. if (sliceRectX(intersectingRect, pendingRect.left, pendingRects) || sliceRectX(intersectingRect, pendingRect.right, pendingRects) || sliceRectY(intersectingRect, pendingRect.top, pendingRects) || sliceRectY(intersectingRect, pendingRect.bottom, pendingRects)) { maskedRects[intersectionIndex] = pendingRect; continue; } // Try to subdivide the pending rect using one of the axes of the intersecting rect. // If this succeeds, the fragments will be requeued and no further action is needed. if (sliceRectX(pendingRect, intersectingRect.left, pendingRects) || sliceRectX(pendingRect, intersectingRect.right, pendingRects) || sliceRectY(pendingRect, intersectingRect.top, pendingRects) || sliceRectY(pendingRect, intersectingRect.bottom, pendingRects)) { continue; } // This should never happen assert(false); return kMiniscriptInstructionOutcomeFailed; } _allClear = (maskedRects.size() == 0); return kMiniscriptInstructionOutcomeContinue; } void XorCheckModifier::recursiveFindXorElements(Structural *structural, Common::Array &elements) { for (const Common::SharedPtr &child : structural->getChildren()) recursiveFindXorElements(child.get(), elements); if (!structural->isElement()) return; Element *element = static_cast(structural); if (!element->isVisual()) return; VisualElement *visual = static_cast(element); if (visual->getRenderProperties().getInkMode() == VisualElementRenderProperties::kInkModeXor) elements.push_back(visual); } bool XorCheckModifier::sliceRectX(const Common::Rect &rect, int32 x, Common::Array &outSlices) { if (x > rect.left && x < rect.right) { Common::Rect leftSlice = Common::Rect(rect.left, rect.top, x, rect.bottom); Common::Rect rightSlice = Common::Rect(x, rect.top, rect.right, rect.bottom); outSlices.push_back(leftSlice); outSlices.push_back(rightSlice); return true; } return false; } bool XorCheckModifier::sliceRectY(const Common::Rect &rect, int32 y, Common::Array &outSlices) { if (y > rect.top && y < rect.bottom) { Common::Rect topSlice = Common::Rect(rect.left, rect.top, rect.right, y); Common::Rect bottomSlice = Common::Rect(rect.left, y, rect.right, rect.bottom); outSlices.push_back(topSlice); outSlices.push_back(bottomSlice); return true; } return false; } ObsidianPlugIn::ObsidianPlugIn(const Common::SharedPtr &wgData) : _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this), _dictionaryModifierFactory(this), _wordMixerModifierFactory(this), _xorModModifierFactory(this), _xorCheckModifierFactory(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); registrar->registerPlugInModifier("xorMod", &_xorModModifierFactory); registrar->registerPlugInModifier("xorCheck", &_xorCheckModifierFactory); } const Common::SharedPtr& ObsidianPlugIn::getWordGameData() const { return _wgData; } WordGameData::WordBucket::WordBucket() : _spacing(0) { } WordGameData::SortedWord::SortedWord() : _chars(nullptr), _length(0) { } 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(const Common::SharedPtr &wgData) { return Common::SharedPtr(new Obsidian::ObsidianPlugIn(wgData)); } } // End of namespace PlugIns } // End of namespace MTropolis