/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "common/hashmap.h" #include "common/path.h" #include "common/file.h" #include "common/debug.h" #include "common/util.h" #include "tetraedge/tetraedge.h" #include "tetraedge/game/character.h" #include "tetraedge/game/application.h" #include "tetraedge/game/game.h" #include "tetraedge/game/character_settings_xml_parser.h" #include "tetraedge/te/te_model_animation.h" #include "tetraedge/te/te_core.h" namespace Tetraedge { /*static*/ Common::HashMap *Character::_globalCharacterSettings = nullptr; /*static*/ Common::HashMap> *Character::_animCacheMap = nullptr; // /*static*/ Common::Array *Character::_animCache = nullptr; // /*static*/ uint Character::_animCacheSize = 0; void Character::CharacterSettings::clear() { _name.clear(); _modelFileName.clear(); _defaultScale = TeVector3f32(); _idleAnimFileName.clear(); _walkSettings.clear(); _walkSpeed = 0.0f; _cutSceneCurveDemiPosition = TeVector3f32(); _defaultEyes.clear(); _defaultMouth.clear(); _defaultBody.clear(); _invertNormals = false; } void Character::WalkSettings::clear() { for (int i = 0; i < 4; i++) { _walkParts[i] = AnimSettings(); } } Character::Character() : _walkCurveStart(0), _lastFrame(-1), _callbacksChanged(false), _notWalkAnim(false), _returnToIdleAnim(false), _walkModeStr("Walk"), _needsSomeUpdate(false), _positionFlag(false), _lookingAtTallThing(false), _stepSound1("sounds/SFX/PAS_H_BOIS1.ogg"), _stepSound2("sounds/SFX/PAS_H_BOIS2.ogg"), _freeMoveZone(nullptr), _animSoundOffset(0), _lastAnimFrame(0), _charLookingAt(nullptr), _recallageY(true), _walkToFlag(false), _walkCurveEnd(0.0f), _walkCurveLast(0.0f), _walkCurveLen(0.0f), _walkCurveIncrement(0.0f), _walkEndAnimG(false), _walkTotalFrames(0), _walkCurveNextLength(0.0f), _walkedLength(0.0f), _walkLoopAnimLen(0.0f), _walkEndGAnimLen(0.0f), _walkStartAnimLen(0.0f), _walkStartAnimFrameCount(0), _walkLoopAnimFrameCount(0), _walkEndGAnimFrameCount(0), _hasAnchor(false), _charLookingAtOffset(0.0f) { _curModelAnim.setDeleteFn(&TeModelAnimation::deleteLaterStatic); } Character::~Character() { _model->setVisible(false); _model->bonesUpdatedSignal().remove(this, &Character::onBonesUpdate); deleteAnim(); Game *game = g_engine->getGame(); Common::Array> &models = game->scene().models(); for (uint i = 0; i < models.size(); i++) { if (models[i] == _model) { models.remove_at(i); break; } } removeAnim(); for (uint s = 0; s < 2; s++) { if (!_shadowModel[s]) continue; for (uint i = 0; i < models.size(); i++) { if (models[i] == _shadowModel[s]) { models.remove_at(i); break; } } } } /*static*/ void Character::cleanup() { if (_globalCharacterSettings) delete _globalCharacterSettings; _globalCharacterSettings = nullptr; animCacheFreeAll(); } void Character::addCallback(const Common::String &animKey, const Common::String &fnName, float triggerFrame, float maxCalls) { Callback *c = new Callback(); c->_luaFn = fnName; c->_lastCheckFrame = 0; c->_triggerFrame = (int)triggerFrame; c->_maxCalls = (int)maxCalls; // Slight difference here to orig (that sets -NAN) because of // the way this gets used later, setting large negative is more correct. c->_callsMade = (maxCalls == -1.0 ? -1e9 : 0.0f); if (g_engine->gameType() == TetraedgeEngine::kSyberia) { // Syberia 1 has slightly weird logic to decide what key to use. // // WORKAROUND: This callback seems to be set too late.. frame 31, but it // only gets to 15? Some bug in the way anim blends hand off? // for scenes/CitSpace2/34230/Logic34230.lua // if (fnName == "ChangeClef" && c->_triggerFrame == 31) c->_triggerFrame = 15; const Common::Path animPath = _model->anim()->loadedPath(); // Another difference.. the original messes with paths a bit - just // use the file name, since it's already limited by character. Common::String animName = animPath.baseName(); if (animName.empty()) animName = animPath.toString(); if (_callbacks.contains(animName)) { _callbacks[animName].push_back(c); } else { Common::Path animKeyPath(animKey); Common::Array callbacks; callbacks.push_back(c); _callbacks.setVal(animKeyPath.baseName(), callbacks); } } else if (g_engine->gameType() == TetraedgeEngine::kSyberia2){ // Syberia 2 is simpler, it always uses a lower-case version of the anim // file in the passed key. Common::String key = Common::Path(animKey).baseName(); key.toLowercase(); if (_callbacks.contains(key)) { _callbacks[key].push_back(c); } else { Common::Array callbacks; callbacks.push_back(c); _callbacks.setVal(key, callbacks); } } else { error("addCallback: Unsupported game type."); } } /*static*/ void Character::animCacheFreeAll() { /* if (_animCache) { for (const auto &entry : (*_animCache)) _animCacheSize -= entry._size; delete _animCache; _animCache = nullptr; } */ if (_animCacheMap) { delete _animCacheMap; _animCacheMap = nullptr; } } /*static*/ void Character::animCacheFreeOldest() { // Unused? //_animCacheSize -= _animCache[_animCache.size() - 1]._size; //_animCache.pop_back(); } /*static*/ TeIntrusivePtr Character::animCacheLoad(const Common::Path &path) { const Common::String pathStr = path.toString(); if (!_animCacheMap) { _animCacheMap = new Common::HashMap>(); } if (_animCacheMap->contains(pathStr)) { // Copy from the cache (keep the cached instance clean) return new TeModelAnimation(*_animCacheMap->getVal(pathStr)); } TeIntrusivePtr modelAnim = new TeModelAnimation(); if (!modelAnim->load(path)) { warning("Failed to load anim %s", path.toString().c_str()); } _animCacheMap->setVal(pathStr, modelAnim); return modelAnim; } float Character::animLength(const TeModelAnimation &modelanim, int bone, int lastframe) { int last = modelanim.lastFrame(); if (lastframe > last) lastframe = last; int first = modelanim.firstFrame(); const TeVector3f32 starttrans = translationVectorFromAnim(modelanim, bone, first); const TeVector3f32 endtrans = translationVectorFromAnim(modelanim, bone, lastframe); const TeVector3f32 secondtrans = translationVectorFromAnim(modelanim, bone, first + 1); return ((endtrans.z() - starttrans.z()) + secondtrans.z()) - starttrans.z(); } float Character::animLengthFromFile(const Common::String &animname, uint32 *pframeCount, uint lastframe /* = 9999 */) { if (animname.empty()) { *pframeCount = 0; return 0.0f; } TeIntrusivePtr anim = _model->anim(); if (!anim->loadedPath().toString().contains(animname)) { Common::Path animpath("models/Anims"); animpath.joinInPlace(animname); anim = animCacheLoad(animpath); if (!anim) error("Character::animLengthFromFile couldn't load anim %s", animname.c_str()); } // The "Pere" or "father" bone is the root. float animLen = animLength(*anim, anim->findBone(rootBone()), lastframe); int frameCount = anim->lastFrame() + 1 - anim->firstFrame(); *pframeCount = frameCount; return animLen * _model->scale().z(); } bool Character::blendAnimation(const Common::String &animname, float amount, bool repeat, bool returnToIdle) { Common::Path animpath("models/Anims"); animpath.joinInPlace(animname); _notWalkAnim = !(animname.contains(_characterSettings._idleAnimFileName) || animname.contains(walkAnim(WalkPart_Start)) || animname.contains(walkAnim(WalkPart_Loop)) || animname.contains(walkAnim(WalkPart_EndG)) || animname.contains(walkAnim(WalkPart_EndD))); if (_curModelAnim) { _curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished); _curModelAnim->unbind(); _curModelAnim->reset(); } _curModelAnim = animCacheLoad(animpath); assert(_curModelAnim); _curModelAnim->reset(); _curModelAnim->onFinished().add(this, &Character::onModelAnimationFinished); _curModelAnim->bind(_model); _model->blendAnim(_curModelAnim, amount, repeat); _lastFrame = -1; _curModelAnim->play(); _curAnimName = animname; _returnToIdleAnim = !repeat && returnToIdle; return true; } TeVector3f32 Character::correctPosition(const TeVector3f32 &pos) { bool flag; TeVector3f32 result = _freeMoveZone->correctCharacterPosition(pos, &flag, true); if (!flag) result.y() = _model->position().y(); return result; } float Character::curveOffset() { return _walkCurveStart; } void Character::deleteAllCallback() { _callbacksChanged = true; for (auto &pair : _callbacks) { for (Callback *c : pair._value) { delete c; } } _callbacks.clear(); } void Character::deleteAnim() { if (_curModelAnim) { _curModelAnim->onFinished().remove(this, &Character::onModelAnimationFinished); _curModelAnim->unbind(); _curModelAnim->reset(); } _model->removeAnim(); _curModelAnim.release(); } void Character::deleteCallback(const Common::String &key, const Common::String &fnName, float f) { _callbacksChanged = true; assert(_model->anim()); Common::String animFile = _model->anim()->loadedPath().baseName(); if (!_callbacks.contains(animFile)) return; Common::Array &cbs = _callbacks.getVal(animFile); for (uint i = 0; i < cbs.size(); i++) { if (fnName.empty()) { delete cbs[i]; // don't remove from array, clear at the end. } else if (cbs[i]->_luaFn == fnName) { if (f == -1 || cbs[i]->_triggerFrame == f) { delete cbs[i]; cbs.remove_at(i); i--; } } } if (fnName.empty()) cbs.clear(); if (cbs.empty()) _callbacks.erase(animFile); } void Character::endMove() { if (g_engine->getGame()->scene()._character == this) walkMode("Walk"); _onFinishedSignal.call(); stop(); } const Character::WalkSettings *Character::getCurrentWalkFiles() { for (const auto & walkSettings : _characterSettings._walkSettings) { if (walkSettings._key == _walkModeStr) return &walkSettings._value; } return nullptr; } bool Character::isFramePassed(int frameno) { return (frameno > _lastAnimFrame && _model->anim()->curFrame2() >= frameno); } bool Character::isWalkEnd() { const Common::String animFile = _model->anim()->loadedPath().baseName(); for (const auto & walkSettings : _characterSettings._walkSettings) { if (walkSettings._value._walkParts[WalkPart_EndD]._file.contains(animFile) || walkSettings._value._walkParts[WalkPart_EndG]._file.contains(animFile)) return true; } return false; } int Character::leftStepFrame(enum Character::WalkPart walkpart) { const Character::WalkSettings *settings = getCurrentWalkFiles(); if (settings) { return settings->_walkParts[(int)walkpart]._stepLeft; } return -1; } int Character::rightStepFrame(enum Character::WalkPart walkpart) { const Character::WalkSettings *settings = getCurrentWalkFiles(); if (settings) { return settings->_walkParts[(int)walkpart]._stepRight; } return -1; } bool Character::loadModel(const Common::String &mname, bool unused) { assert(_globalCharacterSettings); if (_model) { _model->bonesUpdatedSignal().remove(this, &Character::onBonesUpdate); } _model = new TeModel(); _model->bonesUpdatedSignal().add(this, &Character::onBonesUpdate); if (!_globalCharacterSettings->contains(mname)) return false; _characterSettings = _globalCharacterSettings->getVal(mname); _model->setTexturePath("models/Textures"); _model->setEnableLights(true); if (!_model->load(Common::Path("models").join(_characterSettings._modelFileName))) { warning("Failed to load character model %s", _characterSettings._modelFileName.c_str()); return false; } _model->setName(mname); _model->setScale(_characterSettings._defaultScale); if (_characterSettings._invertNormals) _model->invertNormals(); for (auto &mesh : _model->meshes()) mesh->setVisible(true); // Set all mouthes, eyes, etc not visible by default _model->setVisibleByName("_B_", false); _model->setVisibleByName("_Y_", false); _model->setVisibleByName("_M_", false); _model->setVisibleByName("_E_", false); // Note: game loops through "faces" here, but it only ever uses the default ones. _model->setVisibleByName(_characterSettings._defaultEyes, true); _model->setVisibleByName(_characterSettings._defaultMouth, true); _model->setVisibleByName(_characterSettings._defaultBody, true); setAnimation(_characterSettings._idleAnimFileName, true); _walkStartAnimLen = animLengthFromFile(walkAnim(WalkPart_Start), &_walkStartAnimFrameCount); _walkEndGAnimLen = animLengthFromFile(walkAnim(WalkPart_EndG), &_walkEndGAnimFrameCount); _walkLoopAnimLen = animLengthFromFile(walkAnim(WalkPart_Loop), &_walkLoopAnimFrameCount); if (g_engine->gameType() == TetraedgeEngine::kSyberia) { // Only Syberia 1 has the simple shadow. TeIntrusivePtr shadow = Te3DTexture::makeInstance(); TeCore *core = g_engine->getCore(); shadow->load(core->findFile("models/Textures/simple_shadow_alpha.tga")); for (int i = 0; i < 2; i++) { TeModel *pmodel = new TeModel(); _shadowModel[i] = pmodel; pmodel->setName("Shadow"); Common::Array arr; arr.resize(4); arr[0] = TeVector3f32(-60.0, 0.0, -60.0); arr[1] = TeVector3f32(-60.0, 0.0, 60.0); arr[2] = TeVector3f32(60.0, 0.0, -60.0); arr[3] = TeVector3f32(60.0, 0.0, 60.0); pmodel->setQuad(shadow, arr, TeColor(0xff, 0xff, 0xff, 0x50)); } } return true; } /*static*/ bool Character::loadSettings(const Common::Path &path) { CharacterSettingsXmlParser parser; parser.setAllowText(); if (_globalCharacterSettings) delete _globalCharacterSettings; _globalCharacterSettings = new Common::HashMap(); parser.setCharacterSettings(_globalCharacterSettings); // WORKAROUND: This file contains invalid comments // eg, ", offset); if (endOffset != Common::String::npos) { size_t realEndOffset = fixedbuf.find("walk>-->", endOffset); if (realEndOffset != Common::String::npos && realEndOffset > endOffset) { fixedbuf.replace(offset, endOffset - offset, "