3 Commits

6 changed files with 459 additions and 44 deletions
+141
View File
@@ -7,7 +7,9 @@
#include "core/Logger.hpp"
#include "core/Mem.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"
#include "offsets/engine/Gx.hpp"
#include <windows.h>
@@ -20,6 +22,138 @@ namespace moonwell
{
namespace
{
constexpr uint32_t kTraitorFlag = 0x40000000u;
bool g_loginCharacterIsTraitor = false;
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
GxSetProjectionFn g_nextSetProjection = nullptr;
struct CameraTransition
{
float zoom = 1.0f;
float verticalOffset = 0.0f;
float startZoom = 1.0f;
float startVerticalOffset = 0.0f;
float targetZoom = 1.0f;
float targetVerticalOffset = 0.0f;
DWORD startedAt = 0;
DWORD duration = 0;
} g_characterCreateCamera;
void UpdateCharacterCreateCamera(DWORD now)
{
auto& camera = g_characterCreateCamera;
if (!camera.duration)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
return;
}
const DWORD elapsed = now - camera.startedAt;
float t = static_cast<float>(elapsed) / static_cast<float>(camera.duration);
if (t >= 1.0f)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
camera.duration = 0;
return;
}
// Smoothstep avoids a visible jerk at both ends of the move.
t = t * t * (3.0f - 2.0f * t);
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
camera.verticalOffset = camera.startVerticalOffset
+ (camera.targetVerticalOffset - camera.startVerticalOffset) * t;
}
int __cdecl SetCharacterCreateCamera(void* state)
{
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
const DWORD now = GetTickCount();
UpdateCharacterCreateCamera(now);
auto& camera = g_characterCreateCamera;
camera.startZoom = camera.zoom;
camera.startVerticalOffset = camera.verticalOffset;
camera.targetZoom = enabled && faceZoom > 1.0f ? faceZoom : 1.0f;
camera.targetVerticalOffset = enabled ? faceVerticalOffset : 0.0f;
camera.startedAt = now;
camera.duration = static_cast<DWORD>(requestedDuration);
UpdateCharacterCreateCamera(now);
return 0;
}
void __fastcall CharacterCreateProjectionHook(void* self, void* edx, const void* projection)
{
if (!g_nextSetProjection)
return;
UpdateCharacterCreateCamera(GetTickCount());
const auto& camera = g_characterCreateCamera;
if (projection && (camera.zoom != 1.0f || camera.verticalOffset != 0.0f))
{
const float* source = static_cast<const float*>(projection);
if (source[11] > 0.5f)
{
float adjusted[16];
std::memcpy(adjusted, source, sizeof(adjusted));
adjusted[0] *= camera.zoom;
adjusted[5] *= camera.zoom;
adjusted[9] += camera.verticalOffset;
g_nextSetProjection(self, edx, adjusted);
return;
}
}
g_nextSetProjection(self, edx, projection);
}
void InstallCharacterCreateCamera()
{
namespace gx = wxl::offsets::engine::gx;
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
void** slot = &vtable[gx::kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
{
g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed");
return;
}
WLOG_INFO("moonwell: smooth character-create camera installed");
}
int __cdecl SetLoginCharacterFlags(void* state)
{
uint32_t flags = 0;
if (state && wxl::runtime::lua::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
return 1;
}
int __cdecl IsTraitor(void* state)
{
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
return 1;
}
struct Patch
{
const char* name;
@@ -196,7 +330,14 @@ namespace moonwell
{
Registration()
{
wxl::runtime::lua::RegisterFunction(
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
wxl::runtime::lua::RegisterFunction(
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
wxl::runtime::modules::Register(
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
}
} g_registration;
@@ -676,6 +676,44 @@ function CharacterCreate_UpdateGameModeVisibility()
end
end
-- The stock creation scene frames each race at a different height. Projection
-- zoom therefore needs a per-race vertical target to keep the face in the same
-- part of the screen. Values are indexed by GetSelectedSex(): 2 male, 3 female.
local CHARACTER_CREATE_CAMERA_PROFILES = {
HUMAN = { [2] = -0.28, [3] = -0.20 },
ORC = { [2] = -0.50, [3] = -0.30 },
DWARF = { [2] = 0.30, [3] = 0.38 },
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
SCOURGE = { [2] = 0.18, [3] = 0.27 },
TAUREN = { [2] = -0.78, [3] = -0.62 },
GNOME = { [2] = 0.64, [3] = 0.72 },
TROLL = { [2] = -0.70, [3] = -0.55 },
BLOODELF = { [2] = -0.30, [3] = -0.22 },
DRAENEI = { [2] = -0.62, [3] = -0.55 },
};
local function CharacterCreate_GetCameraProfile()
local _, raceFile = GetNameForRace();
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
local sex = GetSelectedSex();
return 2.0, raceProfile and raceProfile[sex] or -0.20;
end
local function CharacterCreate_UpdateCamera(immediate)
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
local mode = showFace and 1 or 0;
if CharacterCreate.cameraMode == mode and not immediate then
return;
end
CharacterCreate.cameraMode = mode;
CharacterCreate:SetCamera(0);
if MoonWellSetCharacterCreateCamera then
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
end
end
local function CharacterCreate_GetEnteredName()
local text = "";
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
CharacterCreate.gameModeSelectionMode = false;
end
CharacterCreate_UpdateCamera();
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
if showGameModes then
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
CharCreatePersonalizeButton:Show();
CharCreateOkayButton:Hide();
CharacterCreate_UpdateGameModeVisibility();
CharacterCreate_UpdateCamera();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
CharacterChangeFixup();
CharacterCreate_CreateGenderButtonTextures();
CharacterCreate_UpdateButtonCheckedStates();
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
hideScheduled = true
HideNameEditFrame:Show()
@@ -1013,7 +1054,7 @@ end
function CharacterCreate_OnHide()
PAID_SERVICE_CHARACTER_ID = nil;
PAID_SERVICE_TYPE = nil;
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
for button, tooltip in pairs(raceTooltips) do
if tooltip then tooltip:Hide() end
@@ -1518,6 +1559,7 @@ end
function CharacterCreate_UpdateModel(self)
UpdateCustomizationScene();
CharacterCreate_UpdateCamera();
self:AdvanceTime();
end
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
CharacterCreate_UpdateButtonCheckedStates();
end
function CharacterCreate_ResetState()
function CharacterCreate_ResetState(immediate)
CharacterCreate.personalizationMode = false;
CharacterCreate.gameModeSelectionMode = false;
CharacterCreate_UpdateCamera(immediate);
CharacterCreateRaceButtonsContainer:Show();
CharacterCreateClassButtonsContainer:Show();
CharacterCreateGenderButtonsContainer:Show();
@@ -1139,6 +1139,12 @@ end
function CharacterSelect_EnterWorld()
PlaySound("gsCharacterSelectionEnterWorld");
StopGlueAmbience();
-- Persist the server-provided game-mode bit across the GlueXML -> FrameXML
-- Lua-state transition. WarcraftXL owns the value for this Wow.exe process.
if ( type(MoonWellSetLoginCharacterFlags) == "function" ) then
local info = { GetCharacterInfo(CharacterSelect.selectedIndex) };
MoonWellSetLoginCharacterFlags(info[11] or 0);
end
EnterWorld();
end
@@ -0,0 +1,202 @@
-- The MoonWell Achievement.dbc intentionally exposes both faction variants.
-- Restore the 3.3.5a client-side faction filter and make traitors use the
-- opposite faction. IDs come from the stock ruRU build 12340 DBC.
local achievementFaction = {}
local function AddFaction(ids, faction)
for id in string.gmatch(ids, "%d+") do
achievementFaction[tonumber(id)] = faction
end
end
AddFaction([[
33 34 35 37 41 58 202 203 206 220 225 230 246 388 433 434 435 436 437 438
439 440 441 442 470 471 472 473 604 610 611 612 613 614 701 707 709 711 713
764 899 907 908 942 948 963 966 969 970 1012 1022 1023 1024 1028 1029 1030
1034 1035 1038 1040 1151 1167 1169 1172 1184 1189 1191 1192 1255 1262 1279
1466 1563 1656 1676 1678 1681 1684 1686 1692 1697 1707 1737 1752 1757 1762
1782 2016 2144 2194 2419 2421 2536 2760 2761 2762 2763 2764 2770 2777 2778
2779 2780 2781 2782 2797 2817 3356 3478 3556 3576 3580 3596 3676 3846 3851
3856 3857 4156 4296 4298 4436 4784 4786
]], "Alliance")
AddFaction([[
224 259 443 444 445 446 447 448 449 450 451 452 453 454 468 469 593 603 615
616 617 618 619 700 706 708 710 712 714 762 763 873 901 909 926 943 965 967
968 971 1005 1006 1011 1025 1026 1027 1031 1032 1033 1036 1037 1039 1041
1164 1168 1170 1173 1175 1203 1251 1252 1271 1272 1273 1274 1280 1356 1357
1358 1359 1360 1502 1657 1677 1680 1682 1683 1685 1691 1693 1698 1783 1784
2017 2145 2192 2195 2200 2420 2476 2497 2537 2765 2766 2767 2768 2769 2771
2776 2783 2784 2785 2786 2787 2788 2798 2816 3357 3557 3577 3581 3597 3656
3677 3778 3957 4079 4176 4177 4256 4297 4437 4785 4790
]], "Horde")
local NativeGetAchievementInfo = GetAchievementInfo
local NativeGetCategoryNumAchievements = GetCategoryNumAchievements
local NativeGetLatestCompletedAchievements = GetLatestCompletedAchievements
local NativeGetTrackedAchievements = GetTrackedAchievements
local NativeGetNumCompletedAchievements = GetNumCompletedAchievements
local NativeGetTotalAchievementPoints = GetTotalAchievementPoints
local categoryCache = {}
local summaryCache = {}
local function GetDisplayFaction()
local faction = UnitFactionGroup("player")
if MoonWell_IsTraitor then
if faction == "Alliance" then
return "Horde"
elseif faction == "Horde" then
return "Alliance"
end
end
return faction
end
local function IsAchievementVisible(id, faction)
local requiredFaction = achievementFaction[id]
return not requiredFaction or not faction or requiredFaction == faction
end
local function BuildCategory(categoryID, showAll)
local faction = GetDisplayFaction()
local key = tostring(categoryID) .. ":" .. tostring(showAll and 1 or 0)
.. ":" .. tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
local cached = categoryCache[key]
if cached then
return cached
end
local nativeCount = NativeGetCategoryNumAchievements(categoryID, showAll)
local indices = {}
local completed = 0
for nativeIndex = 1, nativeCount do
local id, _, _, isCompleted = NativeGetAchievementInfo(categoryID, nativeIndex)
if id and IsAchievementVisible(id, faction) then
indices[#indices + 1] = nativeIndex
if isCompleted then
completed = completed + 1
end
end
end
cached = { indices = indices, completed = completed }
categoryCache[key] = cached
return cached
end
function GetCategoryNumAchievements(categoryID, showAll)
local category = BuildCategory(categoryID, showAll)
return #category.indices, category.completed
end
function GetAchievementInfo(idOrCategory, achievementIndex)
if achievementIndex == nil then
return NativeGetAchievementInfo(idOrCategory)
end
local category = BuildCategory(idOrCategory, false)
local nativeIndex = category.indices[achievementIndex]
if not nativeIndex then
return nil
end
return NativeGetAchievementInfo(idOrCategory, nativeIndex)
end
local function BuildSummary()
local faction = GetDisplayFaction()
local key = tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
local cached = summaryCache[key]
if cached then
return cached
end
local total, completed = NativeGetNumCompletedAchievements()
local points = NativeGetTotalAchievementPoints()
local statisticFlag = ACHIEVEMENT_FLAGS_STATISTIC or 0x00000001
local hiddenFlag = ACHIEVEMENT_FLAGS_HIDDEN or 0x00000002
for id, requiredFaction in pairs(achievementFaction) do
if faction and requiredFaction ~= faction then
local _, _, achievementPoints, isCompleted, _, _, _, _, flags =
NativeGetAchievementInfo(id)
if flags and bit.band(flags, statisticFlag) == 0 then
local isHidden = bit.band(flags, hiddenFlag) ~= 0
if not isHidden or isCompleted then
total = total - 1
end
if isCompleted then
completed = completed - 1
points = points - (achievementPoints or 0)
end
end
end
end
cached = { total = total, completed = completed, points = points }
summaryCache[key] = cached
return cached
end
function GetNumCompletedAchievements()
local summary = BuildSummary()
return summary.total, summary.completed
end
function GetTotalAchievementPoints()
return BuildSummary().points
end
local function FilterAchievementIDs(getter)
local faction = GetDisplayFaction()
local source = { getter() }
local filtered = {}
for i = 1, #source do
if IsAchievementVisible(source[i], faction) then
filtered[#filtered + 1] = source[i]
end
end
return unpack(filtered)
end
if NativeGetLatestCompletedAchievements then
function GetLatestCompletedAchievements()
return FilterAchievementIDs(NativeGetLatestCompletedAchievements)
end
end
if NativeGetTrackedAchievements then
function GetTrackedAchievements()
return FilterAchievementIDs(NativeGetTrackedAchievements)
end
end
function MoonWellAchievementFaction_Refresh()
categoryCache = {}
summaryCache = {}
if AchievementFrame and AchievementFrame:IsShown() then
if AchievementFrameCategories_Update then
AchievementFrameCategories_Update()
end
if AchievementFrameAchievements_ForceUpdate then
AchievementFrameAchievements_ForceUpdate()
elseif AchievementFrameAchievements_Update then
AchievementFrameAchievements_Update()
end
if AchievementFrameSummary_Update then
AchievementFrameSummary_Update()
end
end
end
local events = CreateFrame("Frame")
events:RegisterEvent("ACHIEVEMENT_EARNED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("UNIT_FACTION")
events:SetScript("OnEvent", function(self, event, unit)
if event ~= "UNIT_FACTION" or unit == "player" then
MoonWellAchievementFaction_Refresh()
end
end)
@@ -1,62 +1,85 @@
-- ============================================================================
-- MoonWell Client
-- Server integration: game modes, traitor faction display
-- ============================================================================
-- MoonWell client game-mode integration.
-- WarcraftXL retains the selected character's server-provided charFlags while
-- WoW replaces the GlueXML Lua state with the in-world FrameXML state.
-- Global flag read by FrameXML files to decide which faction icon to show.
-- true = show opposite faction icon (traitor)
-- false = show real faction icon (normal)
MoonWell_IsTraitor = false
-- ============================================================================
-- AIO handler — called by server on login
-- Server: AIO:Handle(player, "MoonWellClient", "Init", gameMode)
-- gameMode: 0 = normal, 1 = traitor
-- ============================================================================
if not AIO then
return
end
local Handler = AIO.AddHandlers("MoonWellClient", {})
local function MoonWell_RefreshFactionIcons()
-- Portrait PvP icon
local function RefreshFactionIcons()
if PlayerFrame_UpdatePvPStatus then
PlayerFrame_UpdatePvPStatus()
end
-- DragonUI PvP micro button: SetupPVPButton() runs on load before AIO responds,
-- so we refresh its DragonUIPVPIcon texture directly after Init.
if PVPFrame and PVPFrame:IsShown() and PVPFrame_Update then
PVPFrame_Update()
end
if BattlefieldFrame and BattlefieldFrame:IsShown() and BattlefieldFrame_Update then
BattlefieldFrame_Update()
end
if PVPMicroButton and PVPMicroButton.DragonUIPVPIcon then
local factionGroup = UnitFactionGroup("player")
if factionGroup then
local displayFaction = MoonWell_IsTraitor
and (factionGroup == "Alliance" and "Horde" or "Alliance")
or factionGroup
local microTexture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
local texture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
local icon = PVPMicroButton.DragonUIPVPIcon
local highlight = PVPMicroButton:GetHighlightTexture()
if displayFaction == "Alliance" then
icon:SetTexture(microTexture)
icon:SetTexture(texture)
icon:SetTexCoord(0, 118/256, 0, 151/256)
if highlight then
highlight:SetTexture(texture)
highlight:SetTexCoord(0, 118/256, 0, 151/256)
end
elseif displayFaction == "Horde" then
icon:SetTexture(microTexture)
icon:SetTexture(texture)
icon:SetTexCoord(118/256, 236/256, 0, 151/256)
end
local hl = PVPMicroButton:GetHighlightTexture()
if hl then
if displayFaction == "Alliance" then
hl:SetTexture(microTexture)
hl:SetTexCoord(0, 118/256, 0, 151/256)
elseif displayFaction == "Horde" then
hl:SetTexture(microTexture)
hl:SetTexCoord(118/256, 236/256, 0, 151/256)
if highlight then
highlight:SetTexture(texture)
highlight:SetTexCoord(118/256, 236/256, 0, 151/256)
end
end
end
end
end
function Handler.Init(_, mode)
MoonWell_IsTraitor = (tonumber(mode) == 1)
MoonWell_RefreshFactionIcons()
local function ReadNativeGameMode()
if type(MoonWellIsTraitor) ~= "function" then
return false
end
local wasTraitor = MoonWell_IsTraitor
MoonWell_IsTraitor = MoonWellIsTraitor() and true or false
if wasTraitor ~= MoonWell_IsTraitor and MoonWellAchievementFaction_Refresh then
MoonWellAchievementFaction_Refresh()
end
RefreshFactionIcons()
return true
end
local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_LOGIN")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("UNIT_FACTION")
events:SetScript("OnEvent", function(self, event, unit)
if event ~= "UNIT_FACTION" or unit == "player" then
ReadNativeGameMode()
end
end)
-- WarcraftXL may publish its Lua globals shortly after addons are loaded.
-- Lua 5.1/WotLK has no C_Timer, so retry cheaply until the native API appears.
local elapsedTotal = 0
events:SetScript("OnUpdate", function(self, elapsed)
elapsedTotal = elapsedTotal + elapsed
if elapsedTotal < 0.25 then
return
end
elapsedTotal = 0
if ReadNativeGameMode() then
self:SetScript("OnUpdate", nil)
end
end)
@@ -1,11 +1,11 @@
## Interface: 30300
## Title: MoonWell Client
## Notes: MoonWell server integration — game modes, traitor faction display
## Version: 1.0.0
## Notes: Native WarcraftXL game-mode and traitor faction display
## Version: 2.0.0
## Author: MoonWell
## SavedVariables: MoonWellClientDB
## Dependencies: AIO_Client
## DefaultState: Enabled
## X-Hidden: 1
AchievementFactions.lua
MoonWellClient.lua