Compare commits
98 Commits
mimo/build
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ce674577 | ||
| e28775372f | |||
| a7d0116753 | |||
| 36bc60f940 | |||
| b6ce446b8c | |||
| 0a990a8880 | |||
| 03539cf947 | |||
| 16bf1555d6 | |||
| e49b1df8d7 | |||
| 2bf607ca1e | |||
| c18583b346 | |||
| 02748d0a03 | |||
| 43be5c5a52 | |||
| e55d9c6ebd | |||
| 9646ce6730 | |||
| 8ae7d778d2 | |||
| 47b6071dee | |||
| 160f2281b7 | |||
| 71fc4ab20b | |||
| ec8a2fc8fd | |||
| d78656ca07 | |||
| 5788925f38 | |||
| ce546e0f42 | |||
| 8a1623dbcd | |||
| 820b8635a6 | |||
| 2ef37d1d61 | |||
| af80963d4f | |||
| fb6ed0d7bb | |||
| 486c98894a | |||
| 8d92ea9905 | |||
| 95f3434c08 | |||
| 6dce93d948 | |||
| be45c7f88b | |||
| 77f2ad4a80 | |||
| 23a89f22b7 | |||
| 0317e72d6e | |||
| 0cf152317d | |||
| 3aa084bdb6 | |||
| e31c8179da | |||
| 48fcadb4e8 | |||
|
|
879cc2a336 | ||
|
|
b2bb253157 | ||
|
|
68d2fa7abb | ||
|
|
b1d86f6a07 | ||
|
|
2d7a531f82 | ||
|
|
d37b9ae074 | ||
| dd05abe4f7 | |||
| 05b01ad771 | |||
| 2adcbd0372 | |||
|
|
25f07dcc63 | ||
|
|
64686d81a3 | ||
|
|
c13b9b52d5 | ||
|
|
fbafa10ee7 | ||
|
|
3e9b9a215a | ||
| 33fbe851b2 | |||
| fe9139262d | |||
| 0b352bc436 | |||
| a03db862fa | |||
| 1cb28fec34 | |||
| b5ae7f4c2c | |||
| 46da2a8c81 | |||
| a9f4a0651f | |||
| 63334387e6 | |||
| 360155f1b6 | |||
| 68a6fad1d6 | |||
| ff7fef4f73 | |||
| 298a2bf3f6 | |||
| 060832a8ed | |||
| 7d78f4bfbf | |||
| 32171242f8 | |||
| b06cdf3fd3 | |||
| f2d7466849 | |||
| e1fab2b5e1 | |||
| e3d383ca12 | |||
| d23583e269 | |||
| 09f6ac53d4 | |||
| 3e8e7c6cd7 | |||
|
|
67d3b784fd | ||
|
|
15b5417ca6 | ||
| 9e00962d82 | |||
| 80e7b5ad16 | |||
| 706ecc2b00 | |||
| 942e9a03c7 | |||
| 83dac15b62 | |||
| 60bc0b3899 | |||
| 155fcc3230 | |||
| fc888e8c75 | |||
| bb7db603f3 | |||
| 55198170f8 | |||
| 5f43ebbaaa | |||
| fb8c2d0bf2 | |||
| c5a1400e2d | |||
| 531a817ce1 | |||
| c59dd90b9b | |||
| 5c5f2032b4 | |||
| c4983aa0d7 | |||
| 61eca3096a | |||
|
|
536744ec21 |
139
app.js
139
app.js
@@ -1,4 +1,4 @@
|
|||||||
import * as THREE from 'three';
|
import ResonanceVisualizer from './nexus/components/resonance-visualizer.js';\nimport * as THREE from 'three';
|
||||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
@@ -58,11 +58,6 @@ let performanceTier = 'high';
|
|||||||
let hermesWs = null;
|
let hermesWs = null;
|
||||||
let wsReconnectTimer = null;
|
let wsReconnectTimer = null;
|
||||||
let wsConnected = false;
|
let wsConnected = false;
|
||||||
// ═══ EVENNIA ROOM STATE ═══
|
|
||||||
let evenniaRoom = null; // {title, desc, exits[], objects[], occupants[], timestamp, roomKey}
|
|
||||||
let evenniaConnected = false;
|
|
||||||
let evenniaStaleTimer = null;
|
|
||||||
const EVENNIA_STALE_MS = 60000; // mark stale after 60s without update
|
|
||||||
let recentToolOutputs = [];
|
let recentToolOutputs = [];
|
||||||
let workshopPanelCtx = null;
|
let workshopPanelCtx = null;
|
||||||
let workshopPanelTexture = null;
|
let workshopPanelTexture = null;
|
||||||
@@ -602,7 +597,7 @@ class PSELayer {
|
|||||||
|
|
||||||
let pseLayer;
|
let pseLayer;
|
||||||
|
|
||||||
let metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
let resonanceViz, metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||||||
let agentFSMs = {};
|
let agentFSMs = {};
|
||||||
|
|
||||||
function setupGOFAI() {
|
function setupGOFAI() {
|
||||||
@@ -671,7 +666,7 @@ async function init() {
|
|||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||||||
|
|
||||||
setupGOFAI();
|
setupGOFAI();\n resonanceViz = new ResonanceVisualizer(scene);
|
||||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
camera.position.copy(playerPos);
|
camera.position.copy(playerPos);
|
||||||
|
|
||||||
@@ -729,7 +724,7 @@ async function init() {
|
|||||||
// Mnemosyne: Periodic GOFAI Optimization
|
// Mnemosyne: Periodic GOFAI Optimization
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.info('[Mnemosyne] Running periodic optimization...');
|
console.info('[Mnemosyne] Running periodic optimization...');
|
||||||
MemoryOptimizer.optimize(SpatialMemory);
|
MemoryOptimizer.decaySpatialMemory(SpatialMemory);
|
||||||
}, 1000 * 60 * 10); // Every 10 minutes
|
}, 1000 * 60 * 10); // Every 10 minutes
|
||||||
|
|
||||||
fetchGiteaData();
|
fetchGiteaData();
|
||||||
@@ -760,8 +755,6 @@ async function init() {
|
|||||||
enterPrompt.addEventListener('click', () => {
|
enterPrompt.addEventListener('click', () => {
|
||||||
enterPrompt.classList.add('fade-out');
|
enterPrompt.classList.add('fade-out');
|
||||||
document.getElementById('hud').style.display = 'block';
|
document.getElementById('hud').style.display = 'block';
|
||||||
const erpPanel = document.getElementById('evennia-room-panel');
|
|
||||||
if (erpPanel) erpPanel.style.display = 'block';
|
|
||||||
setTimeout(() => { enterPrompt.remove(); }, 600);
|
setTimeout(() => { enterPrompt.remove(); }, 600);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
|
|
||||||
@@ -2176,134 +2169,10 @@ function handleHermesMessage(data) {
|
|||||||
else addChatMessage(msg.agent, msg.text, false);
|
else addChatMessage(msg.agent, msg.text, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (data.type && data.type.startsWith('evennia.')) {
|
|
||||||
handleEvenniaEvent(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
// EVENNIA ROOM SNAPSHOT PANEL (Issue #728)
|
|
||||||
// ═══════════════════════════════════════════
|
|
||||||
|
|
||||||
function handleEvenniaEvent(data) {
|
|
||||||
const evtType = data.type;
|
|
||||||
|
|
||||||
if (evtType === 'evennia.room_snapshot') {
|
|
||||||
evenniaRoom = {
|
|
||||||
roomKey: data.room_key || data.room_id || '',
|
|
||||||
title: data.title || 'Unknown Room',
|
|
||||||
desc: data.desc || '',
|
|
||||||
exits: data.exits || [],
|
|
||||||
objects: data.objects || [],
|
|
||||||
occupants: data.occupants || [],
|
|
||||||
timestamp: data.timestamp || new Date().toISOString()
|
|
||||||
};
|
|
||||||
evenniaConnected = true;
|
|
||||||
renderEvenniaRoomPanel();
|
|
||||||
resetEvenniaStaleTimer();
|
|
||||||
} else if (evtType === 'evennia.player_move') {
|
|
||||||
// Movement may indicate current room changed; update location text
|
|
||||||
if (data.to_room) {
|
|
||||||
const locEl = document.getElementById('hud-location-text');
|
|
||||||
if (locEl) locEl.textContent = data.to_room;
|
|
||||||
}
|
|
||||||
} else if (evtType === 'evennia.session_bound') {
|
|
||||||
evenniaConnected = true;
|
|
||||||
renderEvenniaRoomPanel();
|
|
||||||
} else if (evtType === 'evennia.player_join' || evtType === 'evennia.player_leave') {
|
|
||||||
// Refresh occupant display if we have room data
|
|
||||||
if (evenniaRoom) renderEvenniaRoomPanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetEvenniaStaleTimer() {
|
|
||||||
if (evenniaStaleTimer) clearTimeout(evenniaStaleTimer);
|
|
||||||
const dot = document.getElementById('erp-live-dot');
|
|
||||||
const status = document.getElementById('erp-status');
|
|
||||||
if (dot) dot.className = 'erp-live-dot connected';
|
|
||||||
if (status) { status.textContent = 'LIVE'; status.className = 'erp-status online'; }
|
|
||||||
evenniaStaleTimer = setTimeout(() => {
|
|
||||||
if (dot) dot.className = 'erp-live-dot stale';
|
|
||||||
if (status) { status.textContent = 'STALE'; status.className = 'erp-status stale'; }
|
|
||||||
}, EVENNIA_STALE_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEvenniaRoomPanel() {
|
|
||||||
const panel = document.getElementById('evennia-room-panel');
|
|
||||||
if (!panel) return;
|
|
||||||
panel.style.display = 'block';
|
|
||||||
|
|
||||||
const emptyEl = document.getElementById('erp-empty');
|
|
||||||
const roomEl = document.getElementById('erp-room');
|
|
||||||
|
|
||||||
if (!evenniaRoom) {
|
|
||||||
if (emptyEl) emptyEl.style.display = 'flex';
|
|
||||||
if (roomEl) roomEl.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptyEl) emptyEl.style.display = 'none';
|
|
||||||
if (roomEl) roomEl.style.display = 'block';
|
|
||||||
|
|
||||||
const titleEl = document.getElementById('erp-room-title');
|
|
||||||
const descEl = document.getElementById('erp-room-desc');
|
|
||||||
if (titleEl) titleEl.textContent = evenniaRoom.title;
|
|
||||||
if (descEl) descEl.textContent = evenniaRoom.desc;
|
|
||||||
|
|
||||||
renderEvenniaList('erp-exits', evenniaRoom.exits, (item) => {
|
|
||||||
const name = item.key || item.destination_id || item.name || '?';
|
|
||||||
const dest = item.destination_key || item.destination_id || '';
|
|
||||||
return { icon: '→', label: name, extra: dest && dest !== name ? dest : '' };
|
|
||||||
});
|
|
||||||
|
|
||||||
renderEvenniaList('erp-objects', evenniaRoom.objects, (item) => {
|
|
||||||
const name = item.short_desc || item.key || item.id || item.name || '?';
|
|
||||||
return { icon: '◇', label: name };
|
|
||||||
});
|
|
||||||
|
|
||||||
renderEvenniaList('erp-occupants', evenniaRoom.occupants, (item) => {
|
|
||||||
const name = item.character || item.name || item.account || '?';
|
|
||||||
return { icon: '◉', label: name };
|
|
||||||
});
|
|
||||||
|
|
||||||
const tsEl = document.getElementById('erp-footer-ts');
|
|
||||||
const roomKeyEl = document.getElementById('erp-footer-room');
|
|
||||||
if (tsEl) {
|
|
||||||
try {
|
|
||||||
const d = new Date(evenniaRoom.timestamp);
|
|
||||||
tsEl.textContent = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
|
||||||
} catch(e) { tsEl.textContent = '—'; }
|
|
||||||
}
|
|
||||||
if (roomKeyEl) roomKeyEl.textContent = evenniaRoom.roomKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEvenniaList(containerId, items, mapFn) {
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
if (!container) return;
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
if (!items || items.length === 0) {
|
|
||||||
const empty = document.createElement('div');
|
|
||||||
empty.className = 'erp-section-empty';
|
|
||||||
empty.textContent = 'none';
|
|
||||||
container.appendChild(empty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
items.forEach(item => {
|
|
||||||
const mapped = mapFn(item);
|
|
||||||
const row = document.createElement('div');
|
|
||||||
row.className = 'erp-item';
|
|
||||||
row.innerHTML = `<span class="erp-item-icon">${mapped.icon}</span><span>${mapped.label}</span>`;
|
|
||||||
if (mapped.extra) {
|
|
||||||
row.innerHTML += `<span class="erp-item-dest">${mapped.extra}</span>`;
|
|
||||||
}
|
|
||||||
container.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// MNEMOSYNE — LIVE MEMORY BRIDGE
|
// MNEMOSYNE — LIVE MEMORY BRIDGE
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
|||||||
38
index.html
38
index.html
@@ -102,44 +102,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Evennia Room Snapshot Panel -->
|
|
||||||
<div id="evennia-room-panel" class="evennia-room-panel" style="display:none;">
|
|
||||||
<div class="erp-header">
|
|
||||||
<div class="erp-header-left">
|
|
||||||
<div class="erp-live-dot" id="erp-live-dot"></div>
|
|
||||||
<span class="erp-title">EVENNIA — ROOM SNAPSHOT</span>
|
|
||||||
</div>
|
|
||||||
<span class="erp-status" id="erp-status">OFFLINE</span>
|
|
||||||
</div>
|
|
||||||
<div class="erp-body" id="erp-body">
|
|
||||||
<div class="erp-empty" id="erp-empty">
|
|
||||||
<span class="erp-empty-icon">⊘</span>
|
|
||||||
<span class="erp-empty-text">No Evennia connection</span>
|
|
||||||
<span class="erp-empty-sub">Waiting for room data...</span>
|
|
||||||
</div>
|
|
||||||
<div class="erp-room" id="erp-room" style="display:none;">
|
|
||||||
<div class="erp-room-title" id="erp-room-title"></div>
|
|
||||||
<div class="erp-room-desc" id="erp-room-desc"></div>
|
|
||||||
<div class="erp-section">
|
|
||||||
<div class="erp-section-header">EXITS</div>
|
|
||||||
<div class="erp-exits" id="erp-exits"></div>
|
|
||||||
</div>
|
|
||||||
<div class="erp-section">
|
|
||||||
<div class="erp-section-header">OBJECTS</div>
|
|
||||||
<div class="erp-objects" id="erp-objects"></div>
|
|
||||||
</div>
|
|
||||||
<div class="erp-section">
|
|
||||||
<div class="erp-section-header">OCCUPANTS</div>
|
|
||||||
<div class="erp-occupants" id="erp-occupants"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="erp-footer">
|
|
||||||
<span class="erp-footer-ts" id="erp-footer-ts">—</span>
|
|
||||||
<span class="erp-footer-room" id="erp-footer-room"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Top Left: Debug -->
|
<!-- Top Left: Debug -->
|
||||||
<div id="debug-overlay" class="hud-debug"></div>
|
<div id="debug-overlay" class="hud-debug"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,52 @@
|
|||||||
|
|
||||||
class MemoryOptimizer {
|
class MemoryOptimizer {
|
||||||
|
static _lastRun = Date.now();
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.threshold = options.threshold || 0.8;
|
this.threshold = options.threshold || 0.3;
|
||||||
this.decayRate = options.decayRate || 0.05;
|
this.decayRate = options.decayRate || 0.01;
|
||||||
|
this.lastRun = Date.now();
|
||||||
}
|
}
|
||||||
optimize(memory) {
|
|
||||||
console.log('Optimizing memory...');
|
optimize(memories) {
|
||||||
// Heuristic-based pruning
|
const now = Date.now();
|
||||||
return memory.filter(m => m.strength > this.threshold);
|
const elapsed = (now - this.lastRun) / 1000;
|
||||||
|
this.lastRun = now;
|
||||||
|
return memories.map(m => {
|
||||||
|
const decay = (m.importance || 1) * this.decayRate * elapsed;
|
||||||
|
return { ...m, strength: Math.max(0, (m.strength || 1) - decay) };
|
||||||
|
}).filter(m => m.strength > this.threshold || m.locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static decay pass — updates SpatialMemory crystals in-place.
|
||||||
|
* Call as: MemoryOptimizer.decaySpatialMemory(spatialMemoryModule)
|
||||||
|
*/
|
||||||
|
static decaySpatialMemory(spatialMemory, { decayRate = 0.005, threshold = 0.15 } = {}) {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = (now - MemoryOptimizer._lastRun) / 1000;
|
||||||
|
MemoryOptimizer._lastRun = now;
|
||||||
|
|
||||||
|
const memEntries = spatialMemory.getAllMemoryEntries();
|
||||||
|
if (!memEntries || memEntries.length === 0) return 0;
|
||||||
|
|
||||||
|
let decayed = 0;
|
||||||
|
memEntries.forEach(entry => {
|
||||||
|
const currentStrength = entry.mesh?.userData?.strength ?? 0.7;
|
||||||
|
const importance = entry.data?.importance || 1;
|
||||||
|
const decay = importance * decayRate * elapsed;
|
||||||
|
const newStrength = Math.max(0, currentStrength - decay);
|
||||||
|
|
||||||
|
if (newStrength <= threshold && !entry.data?.locked) {
|
||||||
|
spatialMemory.removeMemory(entry.data.id);
|
||||||
|
decayed++;
|
||||||
|
} else if (entry.mesh) {
|
||||||
|
spatialMemory.updateMemory(entry.data.id, { strength: newStrength });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`[MemoryOptimizer] Decay pass: ${decayed} faded, ${memEntries.length - decayed} retained`);
|
||||||
|
return decayed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default MemoryOptimizer;
|
export default MemoryOptimizer;
|
||||||
|
|||||||
@@ -601,6 +601,13 @@ const SpatialMemory = (() => {
|
|||||||
return Object.values(_memoryObjects).map(o => o.data);
|
return Object.values(_memoryObjects).map(o => o.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return full memory entries (mesh + data) for batch operations like decay.
|
||||||
|
*/
|
||||||
|
function getAllMemoryEntries() {
|
||||||
|
return Object.values(_memoryObjects);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── LOCALSTORAGE PERSISTENCE ────────────────────────
|
// ─── LOCALSTORAGE PERSISTENCE ────────────────────────
|
||||||
function _indexHash(index) {
|
function _indexHash(index) {
|
||||||
// Simple hash of memory IDs + count to detect changes
|
// Simple hash of memory IDs + count to detect changes
|
||||||
@@ -863,6 +870,7 @@ const SpatialMemory = (() => {
|
|||||||
return {
|
return {
|
||||||
init, placeMemory, removeMemory, update, importMemories, updateMemory,
|
init, placeMemory, removeMemory, update, importMemories, updateMemory,
|
||||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||||
|
getAllMemoryEntries,
|
||||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||||
exportIndex, importIndex, searchNearby, REGIONS,
|
exportIndex, importIndex, searchNearby, REGIONS,
|
||||||
saveToStorage, loadFromStorage, clearStorage,
|
saveToStorage, loadFromStorage, clearStorage,
|
||||||
|
|||||||
14
nexus/mnemosyne/reasoner.py
Normal file
14
nexus/mnemosyne/reasoner.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
class Reasoner:
|
||||||
|
def __init__(self, rules):
|
||||||
|
self.rules = rules
|
||||||
|
def evaluate(self, entries):
|
||||||
|
return [r['action'] for r in self.rules if self._check(r['condition'], entries)]
|
||||||
|
def _check(self, cond, entries):
|
||||||
|
if cond.startswith('count'):
|
||||||
|
# e.g. count(type=anomaly)>3
|
||||||
|
p = cond.replace('count(', '').split(')')
|
||||||
|
key, val = p[0].split('=')
|
||||||
|
count = sum(1 for e in entries if e.get(key) == val)
|
||||||
|
return eval(f"{count}{p[1]}")
|
||||||
|
return False
|
||||||
22
nexus/mnemosyne/resonance_linker.py
Normal file
22
nexus/mnemosyne/resonance_linker.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
"""Resonance Linker — Finds second-degree connections in the holographic graph."""
|
||||||
|
|
||||||
|
class ResonanceLinker:
|
||||||
|
def __init__(self, archive):
|
||||||
|
self.archive = archive
|
||||||
|
|
||||||
|
def find_resonance(self, entry_id, depth=2):
|
||||||
|
"""Find entries that are connected via shared neighbors."""
|
||||||
|
if entry_id not in self.archive._entries: return []
|
||||||
|
|
||||||
|
entry = self.archive._entries[entry_id]
|
||||||
|
neighbors = set(entry.links)
|
||||||
|
resonance = {}
|
||||||
|
|
||||||
|
for neighbor_id in neighbors:
|
||||||
|
if neighbor_id in self.archive._entries:
|
||||||
|
for second_neighbor in self.archive._entries[neighbor_id].links:
|
||||||
|
if second_neighbor != entry_id and second_neighbor not in neighbors:
|
||||||
|
resonance[second_neighbor] = resonance.get(second_neighbor, 0) + 1
|
||||||
|
|
||||||
|
return sorted(resonance.items(), key=lambda x: x[1], reverse=True)
|
||||||
6
nexus/mnemosyne/rules.json
Normal file
6
nexus/mnemosyne/rules.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"condition": "count(type=anomaly)>3",
|
||||||
|
"action": "alert"
|
||||||
|
}
|
||||||
|
]
|
||||||
112
portal/bannerlord/INSTALL.md
Normal file
112
portal/bannerlord/INSTALL.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Bannerlord Local Install Guide (macOS / Apple Silicon)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Run the GOG Mount & Blade II: Bannerlord build natively on Alexander's Mac (arm64, macOS Sequoia+).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- macOS 14+ on Apple Silicon (arm64)
|
||||||
|
- ~60 GB free disk space (game + Wine prefix)
|
||||||
|
- GOG installer files in `~/Downloads/`:
|
||||||
|
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||||
|
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124)-1.bin` through `-13.bin`
|
||||||
|
|
||||||
|
## Step 1: Install Porting Kit
|
||||||
|
|
||||||
|
Porting Kit (free) wraps Wine/GPTK for macOS. It has a GUI but we automate what we can.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install --cask porting-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch it once to complete first-run setup:
|
||||||
|
```bash
|
||||||
|
open -a "Porting Kit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Create Wine Prefix + Install Game
|
||||||
|
|
||||||
|
**Option A: Via Porting Kit GUI (recommended)**
|
||||||
|
|
||||||
|
1. Open Porting Kit
|
||||||
|
2. Click "Install Game" → "Custom Port" or search for Bannerlord
|
||||||
|
3. Point it at: `~/Downloads/setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||||
|
4. Follow the GOG installer wizard
|
||||||
|
5. Install to default path inside the Wine prefix
|
||||||
|
6. When done, note the prefix path (usually `~/Library/Application Support/PortingKit/...`)
|
||||||
|
|
||||||
|
**Option B: Manual Wine prefix (advanced)**
|
||||||
|
|
||||||
|
If you have Homebrew Wine (or GPTK) installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create prefix
|
||||||
|
export WINEPREFIX="$HOME/Games/Bannerlord"
|
||||||
|
wine64 boot /init
|
||||||
|
|
||||||
|
# Run the GOG installer (it auto-chains the .bin files)
|
||||||
|
cd ~/Downloads
|
||||||
|
wine64 setup_mount__blade_ii_bannerlord_1.3.15.109797_\(64bit\)_\(89124\).exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the GOG installer wizard. Default install path is fine.
|
||||||
|
|
||||||
|
## Step 3: Locate the Game Binary
|
||||||
|
|
||||||
|
After installation, the game executable is at:
|
||||||
|
```
|
||||||
|
$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Or inside Porting Kit's prefix at:
|
||||||
|
```
|
||||||
|
~/Library/Application Support/PortingKit/<prefix-name>/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: First Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the actual path first, then:
|
||||||
|
cd "$HOME/Games/Bannerlord/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client"
|
||||||
|
wine64 Bannerlord.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the launcher script:
|
||||||
|
```bash
|
||||||
|
./portal/bannerlord/launch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Proof (Operator Checklist)
|
||||||
|
|
||||||
|
- [ ] Game window opens and is visible on screen
|
||||||
|
- [ ] At least the main menu renders (TaleWorlds logo, "Campaign", "Custom Battle", etc.)
|
||||||
|
- [ ] Screenshot taken: save to `portal/bannerlord/proof/`
|
||||||
|
- [ ] Launch command recorded below for repeatability
|
||||||
|
|
||||||
|
**Launch command (fill in after install):**
|
||||||
|
```
|
||||||
|
# Repeatable launch:
|
||||||
|
./portal/bannerlord/launch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Black screen on launch:**
|
||||||
|
- Try: `wine64 Bannerlord.exe -force-d3d11` or `-force-vulkan`
|
||||||
|
- Set Windows version: `winecfg` → set to Windows 10
|
||||||
|
|
||||||
|
**Missing DLLs:**
|
||||||
|
- Install DirectX runtime: `winetricks d3dx9 d3dx10 d3dx11 vcrun2019`
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- GPTK/Rosetta overhead is expected; 30-60 FPS is normal on M1/M2
|
||||||
|
- Lower in-game graphics settings to "Medium" for first run
|
||||||
|
|
||||||
|
**Installer won't chain .bin files:**
|
||||||
|
- Make sure all .bin files are in the same directory as the .exe
|
||||||
|
- Verify with: `ls -la ~/Downloads/setup_mount__blade_ii_bannerlord_*`
|
||||||
|
|
||||||
|
## References
|
||||||
|
- GamePortal Protocol: `GAMEPORTAL_PROTOCOL.md`
|
||||||
|
- Portal config: `portals.json` (entry: "bannerlord")
|
||||||
|
- GOG App ID: Mount & Blade II: Bannerlord
|
||||||
|
- Steam App ID: 261550 (for Steam stats integration)
|
||||||
115
portal/bannerlord/launch.sh
Executable file
115
portal/bannerlord/launch.sh
Executable file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bannerlord Launcher for macOS (Apple Silicon via Wine/GPTK)
|
||||||
|
# Usage: ./portal/bannerlord/launch.sh [--wine-prefix PATH] [--exe PATH]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
# Defaults — override with flags or environment
|
||||||
|
WINEPREFIX="${WINEPREFIX:-$HOME/Games/Bannerlord}"
|
||||||
|
BANNERLORD_EXE=""
|
||||||
|
WINE_CMD=""
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--wine-prefix) WINEPREFIX="$2"; shift 2 ;;
|
||||||
|
--exe) BANNERLORD_EXE="$2"; shift 2 ;;
|
||||||
|
--help)
|
||||||
|
echo "Usage: $0 [--wine-prefix PATH] [--exe PATH]"
|
||||||
|
echo ""
|
||||||
|
echo "Defaults:"
|
||||||
|
echo " Wine prefix: $WINEPREFIX"
|
||||||
|
echo " Auto-discovers Bannerlord.exe in the prefix"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Find wine command
|
||||||
|
find_wine() {
|
||||||
|
if command -v wine64 &>/dev/null; then
|
||||||
|
echo "wine64"
|
||||||
|
elif command -v wine &>/dev/null; then
|
||||||
|
echo "wine"
|
||||||
|
elif [ -f "/Applications/Whisky.app/Contents/Resources/WhiskyCmd" ]; then
|
||||||
|
echo "/Applications/Whisky.app/Contents/Resources/WhiskyCmd"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
WINE_CMD="$(find_wine)"
|
||||||
|
if [ -z "$WINE_CMD" ]; then
|
||||||
|
echo "ERROR: No Wine runtime found."
|
||||||
|
echo "Install one of:"
|
||||||
|
echo " brew install --cask porting-kit"
|
||||||
|
echo " brew install --cask crossover"
|
||||||
|
echo " brew tap apple/apple && brew install game-porting-toolkit"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wine runtime: $WINE_CMD"
|
||||||
|
echo "Wine prefix: $WINEPREFIX"
|
||||||
|
|
||||||
|
# Find Bannerlord.exe if not specified
|
||||||
|
if [ -z "$BANNERLORD_EXE" ]; then
|
||||||
|
# Search common GOG install paths
|
||||||
|
SEARCH_PATHS=(
|
||||||
|
"$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||||
|
"$WINEPREFIX/drive_c/GOG Games/Mount Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||||
|
"$WINEPREFIX/drive_c/Program Files/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also search PortingKit prefixes
|
||||||
|
while IFS= read -r -d '' exe; do
|
||||||
|
SEARCH_PATHS+=("$exe")
|
||||||
|
done < <(find "$HOME/Library/Application Support/PortingKit" -name "Bannerlord.exe" -print0 2>/dev/null || true)
|
||||||
|
|
||||||
|
for path in "${SEARCH_PATHS[@]}"; do
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
BANNERLORD_EXE="$path"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$BANNERLORD_EXE" ] || [ ! -f "$BANNERLORD_EXE" ]; then
|
||||||
|
echo "ERROR: Bannerlord.exe not found."
|
||||||
|
echo "Searched:"
|
||||||
|
echo " $WINEPREFIX/drive_c/GOG Games/"
|
||||||
|
echo " ~/Library/Application Support/PortingKit/"
|
||||||
|
echo ""
|
||||||
|
echo "Run the install first. See: portal/bannerlord/INSTALL.md"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Game binary: $BANNERLORD_EXE"
|
||||||
|
echo "Launching..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Log the launch for proof
|
||||||
|
LAUNCH_LOG="$SCRIPT_DIR/proof/launch_$(date +%Y%m%d_%H%M%S).log"
|
||||||
|
mkdir -p "$SCRIPT_DIR/proof"
|
||||||
|
{
|
||||||
|
echo "=== Bannerlord Launch ==="
|
||||||
|
echo "Date: $(date -Iseconds)"
|
||||||
|
echo "Wine: $WINE_CMD"
|
||||||
|
echo "Prefix: $WINEPREFIX"
|
||||||
|
echo "Binary: $BANNERLORD_EXE"
|
||||||
|
echo "User: $(whoami)"
|
||||||
|
echo "macOS: $(sw_vers -productVersion)"
|
||||||
|
echo "Arch: $(uname -m)"
|
||||||
|
echo "========================="
|
||||||
|
} > "$LAUNCH_LOG"
|
||||||
|
echo "Launch log: $LAUNCH_LOG"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Set the prefix and launch
|
||||||
|
export WINEPREFIX
|
||||||
|
EXE_DIR="$(dirname "$BANNERLORD_EXE")"
|
||||||
|
cd "$EXE_DIR"
|
||||||
|
exec "$WINE_CMD" "Bannerlord.exe" "$@"
|
||||||
16
portal/bannerlord/proof/README.md
Normal file
16
portal/bannerlord/proof/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Bannerlord Proof
|
||||||
|
|
||||||
|
Screenshots and launch logs proving the game runs locally on the Mac.
|
||||||
|
|
||||||
|
## How to capture proof
|
||||||
|
|
||||||
|
1. Launch the game: `./portal/bannerlord/launch.sh`
|
||||||
|
2. Wait for main menu to render
|
||||||
|
3. Take screenshot: `screencapture -x portal/bannerlord/proof/main_menu_$(date +%Y%m%d).png`
|
||||||
|
4. Save launch log (auto-generated by launch.sh)
|
||||||
|
|
||||||
|
## Expected proof files
|
||||||
|
|
||||||
|
- `main_menu_*.png` — screenshot of game main menu
|
||||||
|
- `launch_*.log` — launch command + environment details
|
||||||
|
- `ingame_*.png` — optional in-game screenshots
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"rotation": { "y": 0.5 },
|
"rotation": { "y": 0.5 },
|
||||||
"portal_type": "game-world",
|
"portal_type": "game-world",
|
||||||
"world_category": "strategy-rpg",
|
"world_category": "strategy-rpg",
|
||||||
"environment": "production",
|
"environment": "local",
|
||||||
"access_mode": "operator",
|
"access_mode": "operator",
|
||||||
"readiness_state": "downloaded",
|
"readiness_state": "downloaded",
|
||||||
"readiness_steps": {
|
"readiness_steps": {
|
||||||
@@ -37,11 +37,15 @@
|
|||||||
"owner": "Timmy",
|
"owner": "Timmy",
|
||||||
"app_id": 261550,
|
"app_id": 261550,
|
||||||
"window_title": "Mount & Blade II: Bannerlord",
|
"window_title": "Mount & Blade II: Bannerlord",
|
||||||
|
"install_source": "gog",
|
||||||
|
"gog_version": "1.3.15.109797",
|
||||||
|
"launcher_script": "portal/bannerlord/launch.sh",
|
||||||
|
"install_guide": "portal/bannerlord/INSTALL.md",
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": null,
|
"url": null,
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
"action_label": "Enter Calradia",
|
"action_label": "Enter Calradia",
|
||||||
"params": { "world": "calradia" }
|
"params": { "world": "calradia", "runtime": "wine/gptk" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
200
style.css
200
style.css
@@ -2077,203 +2077,3 @@ canvas#nexus-canvas {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ═══ EVENNIA ROOM SNAPSHOT PANEL (Issue #728) ═══ */
|
|
||||||
.evennia-room-panel {
|
|
||||||
position: fixed;
|
|
||||||
right: 20px;
|
|
||||||
top: 80px;
|
|
||||||
width: 300px;
|
|
||||||
background: rgba(5, 5, 16, 0.85);
|
|
||||||
border: 1px solid rgba(74, 240, 192, 0.2);
|
|
||||||
border-right: 3px solid #4af0c0;
|
|
||||||
border-radius: var(--panel-radius);
|
|
||||||
backdrop-filter: blur(var(--panel-blur));
|
|
||||||
font-family: var(--font-body);
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text);
|
|
||||||
z-index: 100;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-bottom: 1px solid rgba(74, 240, 192, 0.12);
|
|
||||||
background: rgba(74, 240, 192, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-live-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-text-muted);
|
|
||||||
transition: background 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-live-dot.connected {
|
|
||||||
background: var(--color-primary);
|
|
||||||
animation: blink 1.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-live-dot.stale {
|
|
||||||
background: var(--color-warning);
|
|
||||||
animation: blink 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-status {
|
|
||||||
font-size: 9px;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: rgba(138, 154, 184, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-status.online {
|
|
||||||
color: var(--color-primary);
|
|
||||||
background: rgba(74, 240, 192, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-status.stale {
|
|
||||||
color: var(--color-warning);
|
|
||||||
background: rgba(255, 170, 34, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-body {
|
|
||||||
padding: 8px 12px;
|
|
||||||
max-height: 360px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty/offline state */
|
|
||||||
.erp-empty {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-empty-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-empty-text {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-empty-sub {
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgba(138, 154, 184, 0.5);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Room content */
|
|
||||||
.erp-room-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-room-desc {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-section {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-section-header {
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
border-bottom: 1px solid rgba(123, 92, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-item {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text);
|
|
||||||
padding: 2px 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-item-icon {
|
|
||||||
color: var(--color-primary);
|
|
||||||
opacity: 0.6;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-item-dest {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-objects .erp-item-icon {
|
|
||||||
color: var(--color-gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-occupants .erp-item-icon {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-section-empty {
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgba(138, 154, 184, 0.4);
|
|
||||||
font-style: italic;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.erp-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-top: 1px solid rgba(74, 240, 192, 0.1);
|
|
||||||
background: rgba(74, 240, 192, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-footer-ts {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.erp-footer-room {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user