/* 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, "