Replace dasher-pack with unified animation-pack using original Blender bone names

This commit is contained in:
2026-06-15 14:28:26 +08:00
parent 9dd3c59edf
commit 844ec194cb
297 changed files with 28680 additions and 1884 deletions
@@ -0,0 +1,156 @@
@tool
class_name McpStructuredLogRing
extends RefCounted
## Head-indexed circular buffer of structured log entries shared by
## game_log_buffer and editor_log_buffer.
##
## Once `_max_lines` (set in subclass `_init`) is reached, new appends
## overwrite the oldest slot at `_head`, keeping append O(1) on overflow
## — the previous slice() approach reallocated the full retained array
## on every drop, which a chatty game would pay for thousands of times
## per second.
##
## Lockless. Subclasses needing thread-safety (editor_log_buffer is
## written from any thread a Godot Logger virtual can fire on) wrap each
## public method with their own Mutex around the `_*_unlocked` helpers.
## Keeping the base lockless means the hot game-side path (single thread,
## called from _process) doesn't pay an unused mutex cost.
##
## Entry shape is owned by subclasses — `_append_entry` takes a
## ready-built Dictionary so each buffer can carry the fields it needs
## (game: `source/level/text`; editor: adds `path/line/function`).
const VALID_LEVELS := ["info", "warn", "error"]
var _max_lines: int
var _storage: Array[Dictionary] = []
## Next write position within `_storage`. While filling (before first
## wrap) equals `_storage.size()`; once full, points at the oldest entry
## (the one about to be overwritten).
var _head := 0
var _dropped_count := 0
## Monotonic number of entries appended since this ring was created. Unlike
## `_storage.size()` and `_dropped_count`, this intentionally survives clear()
## so callers can use it as a stable "next entry to read" cursor.
var _appended_total := 0
func _init(max_lines: int) -> void:
_max_lines = max_lines
## Append `entry` to the ring, evicting the oldest slot when full.
## Subclasses build the dict with their per-source shape and pass it in.
func _append_entry(entry: Dictionary) -> void:
if _storage.size() < _max_lines:
_storage.append(entry)
_head = _storage.size() % _max_lines
else:
## Full — overwrite oldest in place, advance head, count the drop.
_storage[_head] = entry
_head = (_head + 1) % _max_lines
_dropped_count += 1
_appended_total += 1
## Lockless slice. Subclasses with a mutex wrap their `get_range` /
## `get_recent` overrides around this; the lockless base implementations
## of those public methods just delegate here.
func _get_range_unlocked(offset: int, count: int) -> Array[Dictionary]:
var size := _storage.size()
var start := maxi(0, offset)
var stop := mini(size, start + count)
var out: Array[Dictionary] = []
for i in range(start, stop):
out.append(_storage[_logical_to_physical(i)])
return out
func get_range(offset: int, count: int) -> Array[Dictionary]:
return _get_range_unlocked(offset, count)
func get_recent(count: int) -> Array[Dictionary]:
var size := _storage.size()
var start := maxi(0, size - count)
return _get_range_unlocked(start, size - start)
## Lockless cursor read. The cursor is the next sequence to read: calling
## get_since(appended_total()) after a snapshot returns only later appends.
func _get_since_unlocked(since_seq: int, limit: int = -1) -> Dictionary:
var size := _storage.size()
var oldest_seq := _appended_total - size
var start_seq := mini(maxi(since_seq, oldest_seq), _appended_total)
var start := start_seq - oldest_seq
var available := maxi(0, size - start)
var count := available
if limit >= 0:
count = mini(available, limit)
var entries := _get_range_unlocked(start, count)
var next_cursor := start_seq + entries.size()
return {
"cursor": since_seq,
"oldest_cursor": oldest_seq,
"next_cursor": next_cursor,
"appended_total": _appended_total,
"truncated": since_seq < oldest_seq,
"has_more": next_cursor < _appended_total,
"entries": entries,
}
func get_since(since_seq: int, limit: int = -1) -> Dictionary:
return _get_since_unlocked(since_seq, limit)
## Lockless accessors. Subclasses with a mutex use these under their lock
## so the field reads stay encapsulated in the base instead of leaking
## `_storage` / `_dropped_count` reach-through into the subclass.
func _total_count_unlocked() -> int:
return _storage.size()
func _dropped_count_unlocked() -> int:
return _dropped_count
func _appended_total_unlocked() -> int:
return _appended_total
func total_count() -> int:
return _total_count_unlocked()
func dropped_count() -> int:
return _dropped_count_unlocked()
func appended_total() -> int:
return _appended_total_unlocked()
## Translate a logical index (0 = oldest retained) to a physical
## `_storage` slot. Before the first wrap, storage-order is logical-
## order. After wrapping, the oldest entry lives at `_head`.
func _logical_to_physical(logical: int) -> int:
if _storage.size() < _max_lines:
return logical
return (_head + logical) % _max_lines
## Reset the ring to empty. Subclasses with a mutex wrap this with their
## lock; subclasses that surface `clear` to callers (McpEditorLogBuffer)
## return the prior size from their wrapper.
func _clear_storage() -> void:
_storage.clear()
_head = 0
_dropped_count = 0
## Coerce unknown levels to "info" so a misbehaving sender can't poison
## downstream filters with arbitrary strings.
static func _coerce_level(level: String) -> String:
return level if level in VALID_LEVELS else "info"