Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
96 lines
3.4 KiB
C++
96 lines
3.4 KiB
C++
// Combos use the current levels of the arts they are made of (request of
|
|
// 2026-10-18).
|
|
//
|
|
// A combo (cCreatureArts +0x10e, layout in game/engine/arts.h) keeps the arts
|
|
// of its steps and a copy of their levels from when it was made at the
|
|
// Kombomeister; its regeneration total and the steps follow those levels, so
|
|
// raising an art later does not change the combo. Every engine tick the
|
|
// hero's combos are compared with the arts' current levels (cooldown entry
|
|
// level + bonus levels); a combo whose levels are out of date is set again
|
|
// with the game's own cCreatureArts::SetCombo (0x581290), which recomputes the
|
|
// total. The charge keeps its fraction of the total.
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#include "game/engine/arts.h"
|
|
#include "game/engine/events.h"
|
|
#include "game/engine/objects.h"
|
|
#include "re/log.h"
|
|
|
|
namespace game {
|
|
|
|
// cCreatureArts::SetCombo (0x581290, ret 0x10).
|
|
using SetComboFn = void(__fastcall*)(void* arts, void* edx, uint32_t index, const uint32_t* stepArts,
|
|
const uint16_t* levels, uint32_t id);
|
|
|
|
} // namespace game
|
|
|
|
namespace mods::combos {
|
|
namespace {
|
|
|
|
using namespace game; // combo and cooldown layout (arts.h)
|
|
|
|
// The art's level now (cooldown entry level + bonus levels), 0 if unknown.
|
|
uint16_t CurrentLevel(uint8_t* arts, uint32_t art) {
|
|
uint8_t* entry = art ? FindCooldownEntry(arts, static_cast<uint16_t>(art)) : nullptr;
|
|
return entry ? static_cast<uint16_t>(entry[kCooldownLevel] + entry[kCooldownBonusLevels]) : 0;
|
|
}
|
|
|
|
void SyncCombos(uint8_t* hero) {
|
|
uint8_t* arts = hero + kCreatureArts;
|
|
if (hero[kCreatureComboStep] != 0 || arts[kArtsComboRunning] != 0) {
|
|
return; // not while a combo is being executed
|
|
}
|
|
for (unsigned index = 0;; ++index) {
|
|
uint8_t* combo = ComboEntry(arts, index);
|
|
if (!combo) {
|
|
return;
|
|
}
|
|
const uint16_t id = *reinterpret_cast<uint16_t*>(combo + kComboId);
|
|
if (id == 0) {
|
|
continue;
|
|
}
|
|
uint32_t stepArts[kComboSteps];
|
|
uint16_t levels[kComboSteps];
|
|
std::memcpy(stepArts, combo + kComboArts, sizeof(stepArts));
|
|
std::memcpy(levels, combo + kComboLevels, sizeof(levels));
|
|
bool changed = false;
|
|
for (unsigned step = 0; step < kComboSteps; ++step) {
|
|
const uint16_t level = CurrentLevel(arts, stepArts[step]);
|
|
if (level != 0 && level != levels[step]) { // unknown arts keep their level
|
|
levels[step] = level;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!changed) {
|
|
continue;
|
|
}
|
|
const float oldTotal = Float(combo, kComboTotal);
|
|
const float fraction = oldTotal > 0.0f ? Float(combo, kComboCharge) / oldTotal : 1.0f;
|
|
reinterpret_cast<SetComboFn>(0x00581290)(arts, nullptr, index, stepArts, levels, id);
|
|
combo = ComboEntry(arts, index); // SetCombo does not reallocate, but stay safe
|
|
if (combo) {
|
|
Float(combo, kComboCharge) = fraction * Float(combo, kComboTotal);
|
|
re::Log("combos: combo %u set to the current levels %u/%u/%u/%u", index, levels[0], levels[1], levels[2],
|
|
levels[3]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnTick(uint8_t*) {
|
|
if (auto* hero = static_cast<uint8_t*>(GetPlayerHero())) {
|
|
SyncCombos(hero);
|
|
}
|
|
}
|
|
|
|
struct Registration {
|
|
Registration() {
|
|
OnEngineTick(&OnTick);
|
|
}
|
|
} g_registration;
|
|
|
|
} // namespace
|
|
|
|
} // namespace mods::combos
|