mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
1173 lines
37 KiB
C++
1173 lines
37 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 "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<MessageProperties> &msg) {
|
|
if (_enableWhen.respondsTo(msg->getEvent())) {
|
|
Structural *structural = findStructuralOwner();
|
|
if (structural == nullptr || !structural->isElement() || !static_cast<Element *>(structural)->isVisual()) {
|
|
warning("Movement modifier wasn't attached to a visual element");
|
|
return kVThreadError;
|
|
}
|
|
VisualElement *visual = static_cast<VisualElement *>(structural);
|
|
|
|
Common::Rect startRect = visual->getRelativeRect();
|
|
_moveStartPoint = Common::Point(startRect.left, startRect.top);
|
|
_moveStartTime = runtime->getPlayTime();
|
|
|
|
if (!_moveEvent) {
|
|
_runtime = runtime;
|
|
_moveEvent = runtime->getScheduler().scheduleMethod<MovementModifier, &MovementModifier::triggerMove>(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<double>::create(&_rate, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
}
|
|
if (attrib == "frequency") {
|
|
DynamicValueWriteIntegerHelper<int32>::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<int>(_enableWhen.eventType), static_cast<int>(_enableWhen.eventInfo)));
|
|
report->declareDynamic("disableWhen", Common::String::format("Event(%i,%i)", static_cast<int>(_disableWhen.eventType), static_cast<int>(_disableWhen.eventInfo)));
|
|
report->declareDynamic("rate", Common::String::format("%g", _rate));
|
|
report->declareDynamic("frequency", Common::String::format("%i", static_cast<int>(_frequency)));
|
|
report->declareDynamic("type", Common::String::format(_type ? "true" : "false"));
|
|
report->declareDynamic("dest", Common::String::format("(%i,%i)", static_cast<int>(_dest.x), static_cast<int>(_dest.y)));
|
|
report->declareDynamic("triggerEvent", Common::String::format("Event(%i,%i)", static_cast<int>(_triggerEvent.eventType), static_cast<int>(_triggerEvent.eventInfo)));
|
|
}
|
|
#endif
|
|
|
|
Common::SharedPtr<Modifier> MovementModifier::shallowClone() const {
|
|
return Common::SharedPtr<Modifier>(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<Element *>(structural)->isVisual()) {
|
|
warning("Movement modifier wasn't attached to a visual element");
|
|
return;
|
|
}
|
|
VisualElement *visual = static_cast<VisualElement *>(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<double>(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<int32>(round((_dest.x - _moveStartPoint.x) * progression));
|
|
int32 targetY = _moveStartPoint.y + static_cast<int32>(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<MessageProperties> props(new MessageProperties(_triggerEvent, DynamicValue(), visual->getSelfReference()));
|
|
Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(props, visual, true, true, false));
|
|
runtime->sendMessageOnVThread(dispatch);
|
|
} else {
|
|
_moveEvent = runtime->getScheduler().scheduleMethod<MovementModifier, &MovementModifier::triggerMove>(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<MessageProperties> &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<int32>::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<Element *>(structural)->isVisual())
|
|
return;
|
|
|
|
VisualElement *visual = static_cast<VisualElement *>(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<int>(_direction)));
|
|
}
|
|
#endif
|
|
|
|
Common::SharedPtr<Modifier> RectShiftModifier::shallowClone() const {
|
|
Common::SharedPtr<RectShiftModifier> 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<size_t>(firstChar) >= _string.size())
|
|
result.setString("");
|
|
else {
|
|
const size_t availChars = _string.size() - firstChar;
|
|
if (static_cast<size_t>(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<int32>::create(&_firstChar, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
} else if (attrib == "lastchar") {
|
|
DynamicValueWriteIntegerHelper<int32>::create(&_lastChar, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
} else if (attrib == "token") {
|
|
DynamicValueWriteStringHelper::create(&_token, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
} else if (attrib == "firstword") {
|
|
DynamicValueWriteFuncHelper<TextWorkModifier, &TextWorkModifier::scriptSetFirstWord, true>::create(this, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
} else if (attrib == "lastword") {
|
|
DynamicValueWriteFuncHelper<TextWorkModifier, &TextWorkModifier::scriptSetLastWord, true>::create(this, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
}
|
|
|
|
return Modifier::writeRefAttribute(thread, result, attrib);
|
|
}
|
|
|
|
Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
|
|
return Common::SharedPtr<Modifier>(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<ObsidianPlugIn *>(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<DictionaryModifier, &DictionaryModifier::scriptSetIndex, true>::create(this, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
}
|
|
if (attrib == "string") {
|
|
DynamicValueWriteFuncHelper<DictionaryModifier, &DictionaryModifier::scriptSetString, true>::create(this, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
}
|
|
|
|
return Modifier::writeRefAttribute(thread, result, attrib);
|
|
}
|
|
|
|
void DictionaryModifier::resolveStringIndex() {
|
|
if (_isIndexResolved)
|
|
return;
|
|
|
|
_index = 0;
|
|
_isIndexResolved = true;
|
|
|
|
const Common::Array<WordGameData::WordBucket> &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<size_t>(_index) - 1;
|
|
const Common::Array<WordGameData::SortedWord> &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<Modifier> DictionaryModifier::shallowClone() const {
|
|
return Common::SharedPtr<Modifier>(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<const ObsidianPlugIn *>(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<WordMixerModifier, &WordMixerModifier::scriptSetInput, true>::create(this, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
}
|
|
if (attrib == "search") {
|
|
DynamicValueWriteFuncHelper<WordMixerModifier, &WordMixerModifier::scriptSetSearch, true>::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<char> sourceChars;
|
|
for (char c : _input) {
|
|
if (c > ' ')
|
|
sourceChars.push_back(invariantToLower(c));
|
|
}
|
|
|
|
Common::Array<bool> charIsUsed;
|
|
charIsUsed.resize(sourceChars.size());
|
|
|
|
const Common::Array<WordGameData::WordBucket> &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<WordGameData::WordBucket> &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<Modifier> WordMixerModifier::shallowClone() const {
|
|
return Common::SharedPtr<Modifier>(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<MessageProperties> &msg) {
|
|
if (_enableWhen.respondsTo(msg->getEvent())) {
|
|
Structural *owner = findStructuralOwner();
|
|
if (!owner)
|
|
return kVThreadError;
|
|
|
|
if (!owner->isElement())
|
|
return kVThreadReturn;
|
|
|
|
Element *element = static_cast<Element *>(owner);
|
|
if (!element->isVisual())
|
|
return kVThreadReturn;
|
|
|
|
VisualElement *visual = static_cast<VisualElement *>(element);
|
|
|
|
VisualElementRenderProperties renderProps = visual->getRenderProperties();
|
|
renderProps.setInkMode(VisualElementRenderProperties::kInkModeXor);
|
|
|
|
if (_shapeID == 0)
|
|
renderProps.setShape(VisualElementRenderProperties::kShapeRect);
|
|
else
|
|
renderProps.setShape(static_cast<VisualElementRenderProperties::Shape>(VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 + _shapeID - 1));
|
|
|
|
visual->setRenderProperties(renderProps, Common::WeakPtr<GraphicModifier>());
|
|
|
|
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<Modifier> XorModModifier::shallowClone() const {
|
|
return Common::SharedPtr<Modifier>(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<XorCheckModifier, &XorCheckModifier::scriptSetCheckNow, true>::create(this, result);
|
|
return kMiniscriptInstructionOutcomeContinue;
|
|
}
|
|
|
|
return Modifier::writeRefAttribute(thread, result, attrib);
|
|
}
|
|
|
|
Common::SharedPtr<Modifier> XorCheckModifier::shallowClone() const {
|
|
return Common::SharedPtr<Modifier>(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<VisualElement *> 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<Common::Rect> 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<Common::Rect> 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<VisualElement *> &elements) {
|
|
for (const Common::SharedPtr<Structural> &child : structural->getChildren())
|
|
recursiveFindXorElements(child.get(), elements);
|
|
|
|
if (!structural->isElement())
|
|
return;
|
|
|
|
Element *element = static_cast<Element *>(structural);
|
|
if (!element->isVisual())
|
|
return;
|
|
|
|
VisualElement *visual = static_cast<VisualElement *>(element);
|
|
if (visual->getRenderProperties().getInkMode() == VisualElementRenderProperties::kInkModeXor)
|
|
elements.push_back(visual);
|
|
}
|
|
|
|
bool XorCheckModifier::sliceRectX(const Common::Rect &rect, int32 x, Common::Array<Common::Rect> &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<Common::Rect> &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<WordGameData> &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<WordGameData>& 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<size_t> 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::WordBucket> &WordGameData::getWordBuckets() const {
|
|
return _buckets;
|
|
}
|
|
|
|
const Common::Array<WordGameData::SortedWord>& WordGameData::getSortedWords() const {
|
|
return _sortedWords;
|
|
}
|
|
|
|
} // End of namespace ObsidianPlugIn
|
|
|
|
namespace PlugIns {
|
|
|
|
Common::SharedPtr<PlugIn> createObsidian(const Common::SharedPtr<Obsidian::WordGameData> &wgData) {
|
|
return Common::SharedPtr<PlugIn>(new Obsidian::ObsidianPlugIn(wgData));
|
|
}
|
|
|
|
} // End of namespace PlugIns
|
|
|
|
} // End of namespace MTropolis
|