Add marker persistence, proximity respawn, and group sync

Markers now survive zone transitions via persistent MarkerDef structs
(position + area ID) that are NOT cleared during world teardown. The
per-frame tick detects zombie entities (refcount <= 1 from game culling)
and respawns markers when the player approaches within 200 yards.

New DLL Lua APIs: SetMarkerDef, ClearMarkerDef, GetMarkerDef for the
addon to store/query definitions without immediate entity creation.

Addon layer adds group sync via CHAT_MSG_ADDON messages using colon-
delimited protocol (P:idx:x:y:z:area, C:idx, CA, SR, SF:...). Wraps
WorldMarker/ClearWorldMarker to broadcast on placement. Permission
model: raid leader/assist or party leader only.

Fix lua_pushnumber calling convention: Ghidra confirms it is thiscall
(ECX=L, f64 on stack [EBP+8]/[EBP+0xc], ret 8), not fastcall. The
patched inreg fastcall was shoving the f64's low dword into EDX,
producing garbage values like 1.17e-250 from 1810.
This commit is contained in:
MarcelineVQ
2026-03-02 10:38:15 -08:00
parent 7e28c81db0
commit 6778437638
4 changed files with 429 additions and 32 deletions
+5 -1
View File
@@ -107,7 +107,8 @@ pub const lua = struct {
}
pub fn pushnumber(L: State, n: f64) void {
const f: *const fn (State, f64) callconv(fc) void = @ptrFromInt(0x6F3810);
// __thiscall: ECX=L, f64 on stack [EBP+8]/[EBP+0xc], ret 8.
const f: *const fn (State, f64) callconv(.{ .x86_thiscall = .{} }) void = @ptrFromInt(0x6F3810);
f(L, n);
}
@@ -263,6 +264,9 @@ fn registerLuaFunctions() void {
registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker));
registerFunction("GetPlayerPosition", @intFromPtr(&markers.luaGetPlayerPosition));
registerFunction("DistanceToMark", @intFromPtr(&markers.luaDistanceToMark));
registerFunction("SetMarkerDef", @intFromPtr(&markers.luaSetMarkerDef));
registerFunction("ClearMarkerDef", @intFromPtr(&markers.luaClearMarkerDef));
registerFunction("GetMarkerDef", @intFromPtr(&markers.luaGetMarkerDef));
}
}
+232 -3
View File
@@ -1,19 +1,250 @@
-- Markers addon (embedded in DLL, loaded from memory)
-- Part of WeirdUtils - only loaded when markers module is compiled
MARKERS_VERSION = 2
MARKERS_VERSION = 3
BINDING_HEADER_MARKERS = "Markers"
-- =============================================================================
-- Debug logging
-- =============================================================================
local function log(msg)
DEFAULT_CHAT_FRAME:AddMessage("|cff88aaff[WMark]|r " .. msg)
end
-- =============================================================================
-- Addon message protocol
-- Delimiter is ":" (pipe "|" is WoW's escape char for color codes)
-- =============================================================================
local MSG_PREFIX = "WMark"
local NUM_MARKERS = 5
local function getChannel()
if GetNumRaidMembers() > 0 then
return "RAID"
elseif GetNumPartyMembers() > 0 then
return "PARTY"
end
return nil
end
local function canSetMarkers()
if GetNumRaidMembers() > 0 then
for i = 1, 40 do
local name, rank = GetRaidRosterInfo(i)
if name == UnitName("player") then
log("canSet: raid rank=" .. rank .. (rank >= 1 and " YES" or " NO"))
return rank >= 1 -- 1=assist, 2=leader
end
end
log("canSet: not in raid roster")
return false
elseif GetNumPartyMembers() > 0 then
local result = IsPartyLeader()
log("canSet: party leader=" .. tostring(result))
return result
end
log("canSet: solo YES")
return true -- solo: always allowed
end
local function senderHasPermission(sender)
if GetNumRaidMembers() > 0 then
for i = 1, 40 do
local name, rank = GetRaidRosterInfo(i)
if name == sender then
log("perm: " .. sender .. " rank=" .. rank .. (rank >= 1 and " OK" or " NO"))
return rank >= 1
end
end
log("perm: " .. sender .. " not in roster")
return false
elseif GetNumPartyMembers() > 0 then
-- Find the sender's unit ID to check if they're party leader
for i = 1, GetNumPartyMembers() do
if UnitName("party" .. i) == sender then
local isLeader = UnitIsPartyLeader("party" .. i)
log("perm: " .. sender .. "=party" .. i .. " ldr=" .. tostring(isLeader))
return isLeader == 1
end
end
log("perm: " .. sender .. " not in party")
return false
end
log("perm: no group")
return false
end
local function broadcastPlace(index, x, y, z, areaId)
local ch = getChannel()
if not ch then
log("send: no channel")
return
end
local msg = "P:" .. index .. ":" .. x .. ":" .. y .. ":" .. z .. ":" .. areaId
log("SEND [" .. ch .. "] " .. msg)
SendAddonMessage(MSG_PREFIX, msg, ch)
end
local function broadcastClear(index)
local ch = getChannel()
if not ch then return end
local msg = "C:" .. index
log("SEND [" .. ch .. "] " .. msg)
SendAddonMessage(MSG_PREFIX, msg, ch)
end
local function broadcastClearAll()
local ch = getChannel()
if not ch then return end
log("SEND [" .. ch .. "] CA")
SendAddonMessage(MSG_PREFIX, "CA", ch)
end
local function broadcastAllDefs()
local ch = getChannel()
if not ch then return end
local count = 0
for i = 1, NUM_MARKERS do
local x, y, z, areaId = GetMarkerDef(i)
if x then
local msg = "SF:" .. i .. ":" .. x .. ":" .. y .. ":" .. z .. ":" .. areaId
log("SEND [" .. ch .. "] " .. msg)
SendAddonMessage(MSG_PREFIX, msg, ch)
count = count + 1
end
end
log("syncAll: " .. count .. " on " .. ch)
end
-- =============================================================================
-- Wrap DLL functions to broadcast on group placement/clear
-- =============================================================================
local RawWorldMarker = WorldMarker
function WorldMarker(index, ...)
log("WorldMarker(" .. tostring(index) .. ")")
RawWorldMarker(index, unpack(arg))
local ch = getChannel()
if ch then
if canSetMarkers() then
local x, y, z, areaId = GetMarkerDef(index)
if x then
log("def ok, broadcasting")
broadcastPlace(index, x, y, z, areaId)
else
log("GetMarkerDef(" .. index .. ")=nil")
end
else
log("no perm to broadcast")
end
else
log("solo, no broadcast")
end
end
local RawClearWorldMarker = ClearWorldMarker
function ClearWorldMarker(index)
log("ClearWorldMarker(" .. tostring(index) .. ")")
RawClearWorldMarker(index)
if canSetMarkers() and getChannel() then
if index then
broadcastClear(index)
else
broadcastClearAll()
end
end
end
-- =============================================================================
-- Addon message handler
-- =============================================================================
local function onAddonMessage(prefix, message, channel, sender)
if prefix ~= MSG_PREFIX then return end
log("RECV [" .. channel .. "] " .. tostring(sender) .. ": " .. tostring(message))
if sender == UnitName("player") then
log(" own msg, skip")
return
end
if not senderHasPermission(sender) then
log(" no perm, skip")
return
end
-- Parse colon-delimited message
local parts = {}
for part in string.gfind(message, "[^:]+") do
table.insert(parts, part)
end
local cmd = parts[1]
log(" cmd=" .. tostring(cmd) .. " n=" .. table.getn(parts))
if cmd == "P" or cmd == "SF" then
-- Place / Sync Full: P:index:x:y:z:areaId
local idx = tonumber(parts[2])
local x = tonumber(parts[3])
local y = tonumber(parts[4])
local z = tonumber(parts[5])
local areaId = tonumber(parts[6])
if idx and x and y and z and areaId then
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
SetMarkerDef(idx, x, y, z, areaId)
else
log(" PARSE FAIL")
end
elseif cmd == "C" then
local idx = tonumber(parts[2])
if idx then
log(" ClearMarkerDef(" .. idx .. ")")
ClearMarkerDef(idx)
end
elseif cmd == "CA" then
log(" ClearAll")
ClearMarkerDef()
elseif cmd == "SR" then
log(" sync request")
if canSetMarkers() then
broadcastAllDefs()
end
else
log(" unknown: " .. tostring(cmd))
end
end
-- =============================================================================
-- Event frame
-- =============================================================================
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("CHAT_MSG_ADDON")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("RAID_ROSTER_UPDATE")
frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r v" .. MARKERS_VERSION .. " loaded")
elseif event == "CHAT_MSG_ADDON" then
onAddonMessage(arg1, arg2, arg3, arg4)
elseif event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE" then
log("EVT " .. event .. " r=" .. GetNumRaidMembers() .. " p=" .. GetNumPartyMembers())
if canSetMarkers() then
broadcastAllDefs()
end
end
end)
-- =============================================================================
-- Slash commands
-- =============================================================================
SLASH_WORLDMARKER1 = "/worldmarker"
SLASH_WORLDMARKER2 = "/wm"
SlashCmdList["WORLDMARKER"] = function(msg)
@@ -35,10 +266,8 @@ SlashCmdList["WORLDMARKER"] = function(msg)
end
if parts[2] then
-- /wm 1 target
WorldMarker(index, parts[2])
else
-- /wm 1 (cursor position)
WorldMarker(index)
end
end
+184 -28
View File
@@ -2,13 +2,18 @@
//!
//! Manages up to 5 colored markers placed at world positions.
//! Uses CreateEntityInstance_WithAttachment (0x6707c0) for entity lifecycle.
//! Markers persist across zone transitions via MarkerDef definitions.
//! Entities are respawned automatically when the player approaches within 200y.
//!
//! Lua API:
//! WorldMarker(index, x, y, z) — place marker at coordinates
//! WorldMarker(index, "unit") — place marker at unit's position
//! WorldMarker(index) — place marker at cursor terrain position
//! ClearWorldMarker(index) — remove specific marker (1-5)
//! ClearWorldMarker() — remove all markers
//! WorldMarker(index, x, y, z) — place marker at coordinates
//! WorldMarker(index, "unit") — place marker at unit's position
//! WorldMarker(index) — place marker at cursor terrain position
//! ClearWorldMarker(index) — remove specific marker (1-5)
//! ClearWorldMarker() — remove all markers
//! SetMarkerDef(i, x, y, z, area) — store definition (no immediate spawn)
//! ClearMarkerDef([index]) — clear definition (and entity)
//! GetMarkerDef(index) — returns x, y, z, areaId or nil
const std = @import("std");
const hook = @import("zhook");
@@ -68,12 +73,30 @@ pub const Vec3 = struct {
// State
// =============================================================================
/// Persistent marker definition — survives zone transitions.
const MarkerDef = struct {
pos: Vec3,
area_id: u32,
active: bool,
};
const EMPTY_DEF: MarkerDef = .{ .pos = .{ .x = 0, .y = 0, .z = 0 }, .area_id = 0, .active = false };
/// Marker definitions persist across zone transitions (NOT cleared in worldCleanupDetour).
var marker_defs: [NUM_MARKERS]MarkerDef = .{EMPTY_DEF} ** NUM_MARKERS;
/// Transient entity state — cleared on zone transition / teardown.
var marker_entities: [NUM_MARKERS]?*anyopaque = .{null} ** NUM_MARKERS;
var marker_positions: [NUM_MARKERS]?Vec3 = .{null} ** NUM_MARKERS;
var marker_created_tick: [NUM_MARKERS]u32 = .{0} ** NUM_MARKERS;
var hold_queued: [NUM_MARKERS]bool = .{false} ** NUM_MARKERS;
// Proximity respawn constants
const RESPAWN_DISTANCE_SQ: f32 = 200.0 * 200.0;
// Throttle timers for respawn checks
var last_respawn_tick: u32 = 0;
const RESPAWN_CHECK_INTERVAL_MS: u32 = 1000;
var last_zombie_tick: u32 = 0;
const ZOMBIE_CHECK_INTERVAL_MS: u32 = 3000;
// Entities playing their Decay animation before destruction.
// Cleaned up every frame by tickAnimations (via OnWorldUpdate hook).
@@ -123,7 +146,10 @@ const lapi = struct {
}
fn pushnumber(L: u32, n: f64) void {
const f: *const fn (u32, f64) callconv(fc) void = @ptrFromInt(0x6F3810);
// Ghidra confirms: __thiscall(ECX=L), double on stack as [EBP+8]/[EBP+0xc], ret 8.
// Can't use fastcall — our patched inreg would shove the f64's low dword into EDX.
const tc = std.builtin.CallingConvention{ .x86_thiscall = .{} };
const f: *const fn (u32, f64) callconv(tc) void = @ptrFromInt(0x6F3810);
f(L, n);
}
};
@@ -308,13 +334,27 @@ fn beginDespawn(entity: *anyopaque) void {
// =============================================================================
/// Place a marker at the given world position. Replaces any existing marker in that slot.
/// Also stores the definition so the marker survives zone transitions.
/// index is 0-based (0..4).
fn placeMarker(index: usize, pos: Vec3) bool {
if (index >= NUM_MARKERS) return false;
// Clear existing marker in this slot
clearMarker(index);
// Clear existing entity in this slot (but not the def — we're about to overwrite it)
clearEntity(index);
// Store persistent definition
marker_defs[index] = .{
.pos = pos,
.area_id = hook.readMem(u32, o.ZONE_AREA_ID),
.active = true,
};
return spawnEntity(index, pos);
}
/// Spawn a marker entity without touching marker_defs. Used by both
/// placeMarker (initial placement) and the proximity respawn system.
fn spawnEntity(index: usize, pos: Vec3) bool {
var position = [3]f32{ pos.x, pos.y, pos.z + MARKER_Z_OFFSET };
const obj = createEntityInstance(MODEL_PATHS[index], &position, 0.0, 0, 1) orelse {
@@ -323,28 +363,35 @@ fn placeMarker(index: usize, pos: Vec3) bool {
};
marker_entities[index] = obj;
marker_positions[index] = pos;
marker_created_tick[index] = GetTickCount();
hold_queued[index] = false;
con.fmt("[markers] marker {d} placed at {d:.1}, {d:.1}, {d:.1}\n", .{ index + 1, pos.x, pos.y, pos.z });
con.fmt("[markers] marker {d} spawned at {d:.1}, {d:.1}, {d:.1} @0x{x}\n", .{
index + 1, pos.x, pos.y, pos.z, @intFromPtr(obj),
});
return true;
}
/// Remove a specific marker. index is 0-based.
fn clearMarker(index: usize) void {
/// Remove only the live entity for a marker slot (def untouched).
/// Used internally before respawn/replacement.
fn clearEntity(index: usize) void {
if (index >= NUM_MARKERS) return;
cleanupDespawning();
if (marker_entities[index]) |existing| {
beginDespawn(existing);
marker_entities[index] = null;
marker_positions[index] = null;
hold_queued[index] = false;
}
}
/// Remove all markers.
/// Remove a specific marker (entity + definition). index is 0-based.
fn clearMarker(index: usize) void {
if (index >= NUM_MARKERS) return;
clearEntity(index);
marker_defs[index] = EMPTY_DEF;
}
/// Remove all markers (entities + definitions).
fn clearAllMarkers() void {
cleanupDespawning();
var any = false;
@@ -352,9 +399,9 @@ fn clearAllMarkers() void {
if (marker_entities[i]) |existing| {
beginDespawn(existing);
marker_entities[i] = null;
marker_positions[i] = null;
any = true;
}
marker_defs[i] = EMPTY_DEF;
}
if (any) con.print("[markers] all markers cleared\n");
}
@@ -443,14 +490,17 @@ pub fn luaClearWorldMarker(L: u32) callconv(.c) u32 {
// Stand's full-speed grow-in animation across all 5 marker colors.
const HOLD_QUEUE_DELAY_MS: u32 = 2000;
/// Per-frame animation tick (driven by OnWorldUpdate hook).
/// Queues Hold once after Stand has been playing long enough, then cleans up
/// despawning entities.
/// Per-frame tick (driven by OnWorldUpdate hook).
/// 1. Queue Hold animation after Stand grow-in completes
/// 2. Clean up despawning entities
/// 3. Detect zombie entities (culled by the game) and destroy them
/// 4. Respawn markers near the player from persistent definitions
fn tickAnimations() void {
const now = GetTickCount();
cleanupDespawning();
// Queue Hold animation for freshly-created markers
for (0..NUM_MARKERS) |i| {
if (hold_queued[i]) continue;
const entity = marker_entities[i] orelse continue;
@@ -459,6 +509,49 @@ fn tickAnimations() void {
playAnimation(entity, ANIM_HOLD, true);
hold_queued[i] = true;
}
// Zombie detection — entity exists but refcount dropped to 1 (culled by game).
// Check every 3 seconds to avoid per-frame overhead.
if (now -% last_zombie_tick >= ZOMBIE_CHECK_INTERVAL_MS) {
last_zombie_tick = now;
for (0..NUM_MARKERS) |i| {
const entity = marker_entities[i] orelse continue;
const addr = @intFromPtr(entity);
const refcount = hook.readMem(u16, addr + 0x0E);
if (refcount <= 1) {
con.fmt("[markers] zombie detected [{d}] @0x{x} rc={d}, destroying\n", .{ i + 1, addr, refcount });
cleanupEntity(entity);
marker_entities[i] = null;
hold_queued[i] = false;
}
}
}
// Proximity respawn — check every ~1 second
if (now -% last_respawn_tick >= RESPAWN_CHECK_INTERVAL_MS) {
last_respawn_tick = now;
const player = wow.getLocalPlayer();
if (player != 0) {
const player_pos = getUnitPosition(player);
if (player_pos.x != 0 or player_pos.y != 0 or player_pos.z != 0) {
for (0..NUM_MARKERS) |i| {
if (!marker_defs[i].active) continue;
if (marker_entities[i] != null) continue; // entity alive, skip
const dx = player_pos.x - marker_defs[i].pos.x;
const dy = player_pos.y - marker_defs[i].pos.y;
const dz = player_pos.z - marker_defs[i].pos.z;
const dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < RESPAWN_DISTANCE_SQ) {
con.fmt("[markers] respawning [{d}] dist={d:.0}\n", .{ i + 1, @sqrt(dist_sq) });
_ = spawnEntity(i, marker_defs[i].pos);
}
}
}
}
}
}
/// Lua: local x, y, z = GetPlayerPosition()
@@ -484,7 +577,8 @@ pub fn luaDistanceToMark(L: u32) callconv(.c) u32 {
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
const index: usize = @intCast(raw_index - 1);
const mark_pos = marker_positions[index] orelse return 0;
if (!marker_defs[index].active) return 0;
const mark_pos = marker_defs[index].pos;
const player = wow.getLocalPlayer();
if (player == 0) return 0;
@@ -500,6 +594,72 @@ pub fn luaDistanceToMark(L: u32) callconv(.c) u32 {
return 1;
}
/// Lua: SetMarkerDef(index, x, y, z, areaId)
/// Store a marker definition without immediately spawning. Used by the addon
/// when receiving remote marker data — proximity respawn handles entity creation.
pub fn luaSetMarkerDef(L: u32) callconv(.c) u32 {
const nargs = lapi.gettop(L);
if (nargs < 5) return 0;
if (!lapi.isnumber(L, 1) or !lapi.isnumber(L, 2) or !lapi.isnumber(L, 3) or !lapi.isnumber(L, 4) or !lapi.isnumber(L, 5)) return 0;
const raw_index = @as(i32, @intFromFloat(lapi.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
const index: usize = @intCast(raw_index - 1);
const x: f32 = @floatCast(lapi.tonumber(L, 2));
const y: f32 = @floatCast(lapi.tonumber(L, 3));
const z: f32 = @floatCast(lapi.tonumber(L, 4));
const area_id: u32 = @intFromFloat(lapi.tonumber(L, 5));
// Clear existing entity if any (will be respawned by proximity check)
clearEntity(index);
marker_defs[index] = .{
.pos = .{ .x = x, .y = y, .z = z },
.area_id = area_id,
.active = true,
};
con.fmt("[markers] SetMarkerDef [{d}] at {d:.1},{d:.1},{d:.1} area={d}\n", .{ index + 1, x, y, z, area_id });
return 0;
}
/// Lua: ClearMarkerDef([index])
/// Clear a definition (and its entity if alive). No args = clear all.
pub fn luaClearMarkerDef(L: u32) callconv(.c) u32 {
const nargs = lapi.gettop(L);
if (nargs == 0) {
clearAllMarkers();
return 0;
}
if (!lapi.isnumber(L, 1)) return 0;
const raw_index = @as(i32, @intFromFloat(lapi.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
clearMarker(@intCast(raw_index - 1));
return 0;
}
/// Lua: local x, y, z, areaId = GetMarkerDef(index)
/// Returns position and area ID for an active marker def, or nil if inactive.
pub fn luaGetMarkerDef(L: u32) callconv(.c) u32 {
const nargs = lapi.gettop(L);
if (nargs < 1 or !lapi.isnumber(L, 1)) return 0;
const raw_index = @as(i32, @intFromFloat(lapi.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
const index: usize = @intCast(raw_index - 1);
if (!marker_defs[index].active) return 0;
lapi.pushnumber(L, @floatCast(marker_defs[index].pos.x));
lapi.pushnumber(L, @floatCast(marker_defs[index].pos.y));
lapi.pushnumber(L, @floatCast(marker_defs[index].pos.z));
lapi.pushnumber(L, @floatCast(@as(f64, @floatFromInt(marker_defs[index].area_id))));
return 4;
}
// =============================================================================
// World teardown hook
// =============================================================================
@@ -526,15 +686,14 @@ fn worldCleanupDetour() callconv(sc) void {
}
/// Destroy all tracked entities (active markers + despawning).
/// Does NOT clear marker_defs — definitions persist for respawn.
/// Idempotent — safe to call multiple times.
fn destroyAllEntities() void {
var count: u32 = 0;
for (&marker_entities, 0..) |*slot, i| {
if (slot.*) |existing| {
const addr = @intFromPtr(existing);
const flags = hook.readMem(u32, addr + 0x8);
const refcount = hook.readMem(u16, addr + 0x0E);
con.fmt("[markers] destroying marker[{d}] @0x{x} flags=0x{x} refcount={d}\n", .{ i, addr, flags, refcount });
con.fmt("[markers] destroying marker[{d}] @0x{x}\n", .{ i, addr });
cleanupEntity(existing);
slot.* = null;
count += 1;
@@ -542,14 +701,11 @@ fn destroyAllEntities() void {
}
for (&hold_queued) |*h| h.* = false;
for (&marker_created_tick) |*t| t.* = 0;
for (&marker_positions) |*p| p.* = null;
for (&despawning, 0..) |*slot, i| {
if (slot.*) |d| {
const addr = @intFromPtr(d.entity);
const flags = hook.readMem(u32, addr + 0x8);
const refcount = hook.readMem(u16, addr + 0x0E);
con.fmt("[markers] destroying despawn[{d}] @0x{x} flags=0x{x} refcount={d}\n", .{ i, addr, flags, refcount });
con.fmt("[markers] destroying despawn[{d}] @0x{x}\n", .{ i, addr });
cleanupEntity(d.entity);
slot.* = null;
count += 1;
+8
View File
@@ -91,6 +91,14 @@ pub const WF_HIT_TERRAIN_X: usize = 0x360;
pub const WF_HIT_TERRAIN_Y: usize = 0x364;
pub const WF_HIT_TERRAIN_Z: usize = 0x368;
// =============================================================================
// Zone / area identification
// =============================================================================
/// Current zone area ID — numeric, locale-safe zone identifier.
/// Updated by the game as the player moves between areas.
pub const ZONE_AREA_ID: usize = 0x00B4E314;
// =============================================================================
// Per-frame world update
// =============================================================================