Oasis 0.9.5 con Karvan para escritorio

Rama de desarrollo experimental sobre el Oasis de epsylon, sin afiliacion con el
proyecto original. Base: 0.9.5.

Trae el modulo Karvan —salas efimeras en memoria con autodestruccion por TTL, chat
que funciona sin JavaScript y llamadas de audio y video sobre WebRTC— con el enlace
en el menu lateral por el mismo patron que el resto de modulos: renderKarvanLink
calcado de renderPollsLink, entrada en modules_view y en la lista de /modules.

Los estilos del modulo van en su propia hoja, styles/karvan.css, siguiendo el patron
de highlight.css: asi se ve igual con cualquier tema, sin depender del tema movil.

Sin servidores ICE de terceros: no hay STUN de fabrica, porque un STUN aprende la IP
publica y el momento de cada llamada. Solo candidatos host salvo que se configure un
TURN propio en oasis-config.json, con credenciales efimeras por el mecanismo REST de
coturn.

El codigo propio vive separado —views/fork/, backend/fork_routes.js,
models/karvan_model.js y translations/fork/— de modo que sobre los ficheros de
upstream solo hay enganches de una linea y cada version nueva se integra sin
arrastrar nada.

Respecto al arbol de Android: fuera el wrapper y main.js, que es su arranque; el
tema vuelve a Dark-SNH y se restauran los modulos que en movil se recortan por peso.

Verificado arrancando sin OASIS_MOBILE: nueve rutas OK, menu lateral de upstream con
27 grupos y el enlace de Karvan, cero rastro de la interfaz movil (ni hexagonos, ni
topbar, ni barra inferior), y una sala creada y servida.
This commit is contained in:
SITO 2026-08-19 00:35:08 +02:00
commit ddd2787b73
286 changed files with 157145 additions and 0 deletions

View file

@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Neon Infiltrator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-tap-highlight-color: transparent; }
html, body { overflow: hidden; }
body { background: #000; color: #eee; font-family: monospace; display: flex; flex-direction: column; align-items: center; height: 100vh; }
#topbar { width: 100%; padding: 8px 16px; display: flex; align-items: center; gap: 16px; background: #111; border-bottom: 1px solid #333; }
#topbar a { color: #FFA500; text-decoration: none; font-size: 14px; }
#ui { display: flex; gap: 16px; padding: 6px 8px; font-size: 14px; color: #FFA500; flex-wrap: wrap; justify-content: center; width: 100%; }
canvas { display: block; width: 100%; height: auto; max-width: 700px; border: 1px solid #333; image-rendering: crisp-edges; }
#msg { font-size: 13px; color: #FFA500; margin: 3px; text-align: center; min-height: 18px; }
#dpad { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 4px; }
.dpad-row { display: flex; gap: 4px; }
#dpad button { width: 50px; height: 50px; background: #1a0d00; border: 1px solid #FF6600; color: #FF6600; font-size: 20px; cursor: pointer; border-radius: 4px; }
#dpad button:active { background: #3a1a00; }
#controls { color: #666; font-size: 12px; text-align: center; margin-bottom: 3px; }
#scoreSubmit { text-align: center; margin: 6px; }
#scoreSubmit button { background: #1a3a1a; border: 1px solid #FFA500; color: #FFA500; padding: 6px 16px; cursor: pointer; font-family: monospace; font-size: 14px; }
@media (min-width: 768px) { #dpad, #controls { display: none; } }
</style>
</head>
<body>
<div id="topbar">
<a href="/games" target="_top">&#8592; Back to Games</a>
<span style="color:#FFA500;font-weight:bold">NEON INFILTRATOR</span>
</div>
<div id="ui">
<span>DATA: <b id="dataEl">0/0</b></span>
<span>LEVEL: <b id="levelEl">1</b></span>
<span>SCORE: <b id="scoreEl">0</b></span>
<span>BEST: <b id="bestEl">-</b></span>
<span>ENEMIES: <b id="enemyEl">0</b></span>
</div>
<canvas id="c"></canvas>
<div id="msg">Press any arrow key or tap to start</div>
<div id="dpad">
<div class="dpad-row"><button id="btn-up">&#8593;</button></div>
<div class="dpad-row">
<button id="btn-left">&#8592;</button>
<button id="btn-down">&#8595;</button>
<button id="btn-right">&#8594;</button>
</div>
</div>
<div id="controls">Arrow keys / WASD &nbsp;|&nbsp; SPACE — new game</div>
<div id="scoreSubmit" style="display:none">
<form method="POST" action="/games/submit-score" target="_top">
<input type="hidden" name="game" value="neoninfiltrator">
<input type="hidden" id="scoreInput" name="score" value="0">
<button type="submit">Submit Score to Hall of Fame</button>
</form>
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const CELL = 28;
let MAP_W = 0, MAP_H = 0;
let walls = [], enemies = [], dataItems = [], exitPos = { x: -1, y: -1 };
let player = { x: 1, y: 1 };
let level = 1, dataCollected = 0, totalData = 0;
let score = 0;
let best = parseInt(localStorage.getItem('ni_best') || '-1');
let gameState = 'idle';
let lastPlayerMove = 0, lastEnemyStep = 0;
const MOVE_DELAY = 140, ENEMY_INTERVAL = 500;
const keys = {};
function setMsg(txt) { document.getElementById('msg').textContent = txt; }
function updateUI() {
document.getElementById('dataEl').textContent = dataCollected + '/' + totalData;
document.getElementById('levelEl').textContent = level;
document.getElementById('scoreEl').textContent = score;
document.getElementById('bestEl').textContent = best < 0 ? '-' : best;
document.getElementById('enemyEl').textContent = enemies.length;
}
function resizeCanvas() {
let bw = 18, bh = 14;
if (level >= 5) { bw = 22; bh = 18; }
else if (level >= 3) { bw = 20; bh = 16; }
MAP_W = bw; MAP_H = bh;
canvas.width = MAP_W * CELL;
canvas.height = MAP_H * CELL;
}
function generateLevel() {
resizeCanvas();
walls = Array(MAP_H).fill(null).map(() => Array(MAP_W).fill(false));
for (let i = 0; i < MAP_W; i++) { walls[0][i] = true; walls[MAP_H - 1][i] = true; }
for (let i = 0; i < MAP_H; i++) { walls[i][0] = true; walls[i][MAP_W - 1] = true; }
for (let i = 0; i < Math.floor(MAP_W * MAP_H / 7); i++) {
const x = 2 + Math.floor(Math.random() * (MAP_W - 4));
const y = 2 + Math.floor(Math.random() * (MAP_H - 4));
if (!walls[y][x] && !(Math.abs(x - player.x) < 2 && Math.abs(y - player.y) < 2)) {
if (Math.random() < 0.55) walls[y][x] = true;
}
}
for (let i = 0; i < MAP_W * 2; i++) {
const x = 1 + Math.floor(Math.random() * (MAP_W - 2));
const y = 1 + Math.floor(Math.random() * (MAP_H - 2));
if (walls[y][x] && x > 1 && x < MAP_W - 2 && y > 1 && y < MAP_H - 2) {
if (Math.random() < 0.5) walls[y][x] = false;
}
}
player.x = 1; player.y = 1;
if (walls[player.y][player.x]) {
outer: for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
if (!walls[y][x]) { player.x = x; player.y = y; break outer; }
}
}
}
exitPos = { x: MAP_W - 2, y: MAP_H - 2 };
for (let t = 0; t < 200; t++) {
const ex = MAP_W - 2 - Math.floor(Math.random() * 3);
const ey = MAP_H - 2 - Math.floor(Math.random() * 3);
if (ex >= 1 && ey >= 1 && !walls[ey][ex] && (Math.abs(ex - player.x) + Math.abs(ey - player.y)) > 6) {
exitPos = { x: ex, y: ey }; break;
}
}
totalData = Math.min(12, 3 + Math.floor(level / 1.5));
dataItems = [];
for (let i = 0; i < totalData; i++) {
let placed = false;
for (let t = 0; t < 100; t++) {
const dx = 1 + Math.floor(Math.random() * (MAP_W - 2));
const dy = 1 + Math.floor(Math.random() * (MAP_H - 2));
if (!walls[dy][dx] && !(dx === player.x && dy === player.y) && !(dx === exitPos.x && dy === exitPos.y) && !dataItems.some(d => d.x === dx && d.y === dy)) {
dataItems.push({ x: dx, y: dy, collected: false }); placed = true; break;
}
}
if (!placed) {
outer2: for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
if (!walls[y][x] && !(x === player.x && y === player.y) && !(x === exitPos.x && y === exitPos.y) && !dataItems.some(d => d.x === x && d.y === y)) {
dataItems.push({ x, y, collected: false }); break outer2;
}
}
}
}
}
dataCollected = 0;
const enemyCount = Math.min(6, 1 + Math.floor(level / 2));
enemies = [];
for (let i = 0; i < enemyCount; i++) {
let placed = false;
for (let t = 0; t < 150; t++) {
const ex = 2 + Math.floor(Math.random() * (MAP_W - 4));
const ey = 2 + Math.floor(Math.random() * (MAP_H - 4));
if (!walls[ey][ex] && (Math.abs(ex - player.x) + Math.abs(ey - player.y)) > 4 && !(ex === exitPos.x && ey === exitPos.y) && !dataItems.some(d => d.x === ex && d.y === ey)) {
enemies.push({ x: ex, y: ey }); placed = true; break;
}
}
if (!placed) {
outer3: for (let y = 2; y < MAP_H - 2; y++) {
for (let x = 2; x < MAP_W - 2; x++) {
if (!walls[y][x] && (Math.abs(x - player.x) + Math.abs(y - player.y)) > 3 && !enemies.some(e => e.x === x && e.y === y)) {
enemies.push({ x, y }); break outer3;
}
}
}
}
}
updateUI();
setMsg(`Level ${level} — Collect ${totalData} data items and reach the exit`);
}
function checkEnemyCollision() {
for (const e of enemies) {
if (e.x === player.x && e.y === player.y) { endGame(); return true; }
}
return false;
}
function tryMove(dx, dy) {
if (gameState !== 'play') return;
const nx = player.x + dx, ny = player.y + dy;
if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H || walls[ny][nx]) return;
player.x = nx; player.y = ny;
for (const d of dataItems) {
if (!d.collected && d.x === player.x && d.y === player.y) {
d.collected = true; dataCollected++; score += 100;
updateUI(); setMsg(`Data collected! (${dataCollected}/${totalData})`); break;
}
}
if (dataCollected === totalData && exitPos.x === player.x && exitPos.y === player.y) {
const bonus = level * 50;
score += bonus;
updateUI();
setMsg(`Level ${level} complete! +${bonus} bonus. Advancing...`);
gameState = 'transit';
setTimeout(() => { level++; generateLevel(); gameState = 'play'; lastPlayerMove = performance.now(); lastEnemyStep = performance.now(); }, 1400);
return;
}
checkEnemyCollision();
}
function moveEnemies() {
if (gameState !== 'play') return;
for (const e of enemies) {
let dx = 0, dy = 0;
if (Math.random() < 0.6) {
if (Math.abs(e.x - player.x) > Math.abs(e.y - player.y)) dx = e.x > player.x ? -1 : 1;
else dy = e.y > player.y ? -1 : 1;
} else {
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
[dx, dy] = dirs[Math.floor(Math.random() * 4)];
}
const nx = e.x + dx, ny = e.y + dy;
if (nx >= 0 && nx < MAP_W && ny >= 0 && ny < MAP_H && !walls[ny][nx] && !(nx === exitPos.x && ny === exitPos.y)) {
e.x = nx; e.y = ny;
} else {
for (const [sx, sy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
const bx = e.x + sx, by = e.y + sy;
if (bx >= 0 && bx < MAP_W && by >= 0 && by < MAP_H && !walls[by][bx] && !(bx === exitPos.x && by === exitPos.y)) {
e.x = bx; e.y = by; break;
}
}
}
}
checkEnemyCollision();
updateUI();
}
function endGame() {
gameState = 'over';
if (best < 0 || score > best) { best = score; localStorage.setItem('ni_best', best); }
updateUI();
setMsg(`DETECTED! Score: ${score}. Press SPACE for new game.`);
document.getElementById('scoreInput').value = score;
document.getElementById('scoreSubmit').style.display = 'block';
}
function newGame() {
score = 0; level = 1;
document.getElementById('scoreSubmit').style.display = 'none';
generateLevel();
gameState = 'play';
lastPlayerMove = performance.now();
lastEnemyStep = performance.now();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#1a0d00'; ctx.lineWidth = 0.6;
for (let i = 0; i <= MAP_W; i++) {
ctx.beginPath(); ctx.moveTo(i * CELL, 0); ctx.lineTo(i * CELL, canvas.height); ctx.stroke();
}
for (let i = 0; i <= MAP_H; i++) {
ctx.beginPath(); ctx.moveTo(0, i * CELL); ctx.lineTo(canvas.width, i * CELL); ctx.stroke();
}
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
if (walls[y][x]) {
ctx.fillStyle = '#1a0a05';
ctx.fillRect(x * CELL, y * CELL, CELL - 0.5, CELL - 0.5);
ctx.strokeStyle = '#FF6600'; ctx.lineWidth = 1;
ctx.strokeRect(x * CELL, y * CELL, CELL - 0.5, CELL - 0.5);
}
}
}
for (const d of dataItems) {
if (!d.collected) {
ctx.fillStyle = '#FFB347'; ctx.shadowBlur = 6; ctx.shadowColor = '#FF6600';
ctx.beginPath(); ctx.arc(d.x * CELL + CELL / 2, d.y * CELL + CELL / 2, CELL * 0.25, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#FF884D';
ctx.beginPath(); ctx.arc(d.x * CELL + CELL / 2, d.y * CELL + CELL / 2, CELL * 0.12, 0, Math.PI * 2); ctx.fill();
ctx.shadowBlur = 0;
}
}
ctx.fillStyle = '#FFA500'; ctx.globalAlpha = 0.7;
ctx.fillRect(exitPos.x * CELL + 4, exitPos.y * CELL + 4, CELL - 8, CELL - 8);
ctx.globalAlpha = 1; ctx.fillStyle = '#FFA500';
ctx.font = `${CELL - 6}px monospace`;
ctx.fillText('\u25C8', exitPos.x * CELL + CELL * 0.28, exitPos.y * CELL + CELL * 0.78);
if (dataCollected === totalData && gameState === 'play') {
ctx.fillStyle = '#FFB347'; ctx.shadowBlur = 4; ctx.shadowColor = '#FF6600';
ctx.font = 'bold 14px monospace'; ctx.textAlign = 'center';
ctx.fillText('\u2192 EXIT AVAILABLE \u2190', canvas.width / 2, 20);
ctx.shadowBlur = 0; ctx.textAlign = 'left';
}
for (const e of enemies) {
ctx.fillStyle = '#FF4400'; ctx.shadowBlur = 6; ctx.shadowColor = '#FF2200';
ctx.fillRect(e.x * CELL + 4, e.y * CELL + 4, CELL - 8, CELL - 8);
ctx.shadowBlur = 0;
}
ctx.fillStyle = '#FF6600'; ctx.shadowBlur = 8; ctx.shadowColor = '#FF6600';
ctx.fillRect(player.x * CELL + 6, player.y * CELL + 6, CELL - 12, CELL - 12);
ctx.fillStyle = '#FFB347';
ctx.fillRect(player.x * CELL + 10, player.y * CELL + 10, 5, 5);
ctx.shadowBlur = 0;
}
function gameLoop(now) {
if (gameState === 'play') {
let dx = 0, dy = 0;
if (keys.ArrowUp || keys.w) dy = -1;
if (keys.ArrowDown || keys.s) dy = 1;
if (keys.ArrowLeft || keys.a) dx = -1;
if (keys.ArrowRight || keys.d) dx = 1;
if ((dx !== 0 || dy !== 0) && now - lastPlayerMove >= MOVE_DELAY) {
tryMove(dx, dy); lastPlayerMove = now;
}
if (now - lastEnemyStep >= ENEMY_INTERVAL) { moveEnemies(); lastEnemyStep = now; }
}
draw();
requestAnimationFrame(gameLoop);
}
document.addEventListener('keydown', e => {
const key = e.key;
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 's', 'a', 'd', ' '].includes(key)) e.preventDefault();
if (key === ' ') { if (gameState === 'idle' || gameState === 'over') { newGame(); } return; }
keys[key] = true;
if (gameState === 'idle') newGame();
});
document.addEventListener('keyup', e => { keys[e.key] = false; });
const dpadStart = () => { if (gameState === 'idle' || gameState === 'over') newGame(); };
document.getElementById('btn-up').addEventListener('click', () => { dpadStart(); tryMove(0, -1); });
document.getElementById('btn-down').addEventListener('click', () => { dpadStart(); tryMove(0, 1); });
document.getElementById('btn-left').addEventListener('click', () => { dpadStart(); tryMove(-1, 0); });
document.getElementById('btn-right').addEventListener('click', () => { dpadStart(); tryMove(1, 0); });
generateLevel();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>

View file

@ -0,0 +1,73 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 220" style="background:#050505">
<rect width="400" height="220" fill="#050505"/>
<line x1="28" y1="10" x2="28" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="56" y1="10" x2="56" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="84" y1="10" x2="84" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="112" y1="10" x2="112" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="140" y1="10" x2="140" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="168" y1="10" x2="168" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="196" y1="10" x2="196" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="224" y1="10" x2="224" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="252" y1="10" x2="252" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="280" y1="10" x2="280" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="308" y1="10" x2="308" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="336" y1="10" x2="336" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="364" y1="10" x2="364" y2="195" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="0" y1="38" x2="392" y2="38" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="0" y1="66" x2="392" y2="66" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="0" y1="94" x2="392" y2="94" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="0" y1="122" x2="392" y2="122" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="0" y1="150" x2="392" y2="150" stroke="#1a0d00" stroke-width="0.6"/>
<line x1="0" y1="178" x2="392" y2="178" stroke="#1a0d00" stroke-width="0.6"/>
<rect x="0" y="10" width="392" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="0" y="37.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="0" y="65" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="0" y="92.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="0" y="120" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="0" y="147.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="364.5" y="37.5" width="27.5" height="140" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="56" y="37.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="112" y="65" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="140" y="37.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="196" y="92.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="224" y="65" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="252" y="120" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="280" y="37.5" width="27.5" height="55" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="308" y="120" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="168" y="147.5" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<rect x="84" y="120" width="27.5" height="27.5" fill="#1a0a05" stroke="#FF6600" stroke-width="1"/>
<circle cx="71" cy="51" r="6" fill="#FFB347">
<animate attributeName="opacity" values="1;0.4;1" dur="1.2s" repeatCount="indefinite"/>
</circle>
<circle cx="71" cy="51" r="3" fill="#FF884D"/>
<circle cx="183" cy="107" r="6" fill="#FFB347">
<animate attributeName="opacity" values="0.4;1;0.4" dur="1.2s" repeatCount="indefinite"/>
</circle>
<circle cx="183" cy="107" r="3" fill="#FF884D"/>
<circle cx="323" cy="79" r="6" fill="#FFB347">
<animate attributeName="opacity" values="1;0.4;1" dur="0.9s" repeatCount="indefinite"/>
</circle>
<circle cx="323" cy="79" r="3" fill="#FF884D"/>
<rect x="212" y="129" width="12" height="12" fill="#FFA500" opacity="0.7"/>
<text x="218" y="140" font-family="monospace" font-size="11" fill="#FFA500" text-anchor="middle">&#9672;</text>
<rect x="237" y="46" width="20" height="20" fill="#FF4400">
<animate attributeName="opacity" values="1;0.6;1" dur="0.8s" repeatCount="indefinite"/>
</rect>
<rect x="353" y="101" width="20" height="20" fill="#FF4400">
<animate attributeName="opacity" values="0.6;1;0.6" dur="0.8s" repeatCount="indefinite"/>
</rect>
<rect x="35" y="46" width="18" height="18" fill="#FF6600">
<animate attributeName="opacity" values="1;0.7;1" dur="1s" repeatCount="indefinite"/>
</rect>
<rect x="39" y="50" width="7" height="7" fill="#FFB347"/>
<text x="200" y="212" font-family="'Courier New', monospace" font-size="13" fill="#FFA500" text-anchor="middle" font-weight="bold" letter-spacing="3">NEON INFILTRATOR</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB