From 0c2a9acf564972c171e93dafeb52f18333b657d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 18:26:24 +0300 Subject: [PATCH 1/3] fix(milkdrop): read preset files the way MilkDrop reads them With fidelity on, a preset file now goes through a reader that follows MilkDrop's own (parseMilkMd2; state.cpp _GetLineByName, the GetFast readers, ReadCode and the Import functions, the same in the D3D9 code). With fidelity off the old parser stays. MilkDrop indexes the file line by line - a line's name runs to the first '=', space or line break - and looks the keys up in its own order, trying the line after the previous read before scanning from the top. As a result: - Keys are case-sensitive. - An indented line has an empty name and is never read. - `key value` is read, and `key = value` is not. - A key written twice takes the next line or the first occurrence, not the last. - Integer keys are %d, so textured=0.05 is off where we had it on. - Float keys take the leading number, so `.975;` is 0.975. - Numbered code ends at the first missing number. - Equation lines lose `//` and `\` comments and are glued with nothing between them. Readings differ in something the engine uses (readingsDiffer) for 32 corpus presets and none of ours. The Preset records which reader built it (readAcc), and every Preset built in src names its rule. Turning fidelity over rebuilds the running preset only when the two readings differ, and the stage choice reads the file the same way. Not done on purpose: - The two byte rules (0xFF ends the file, values over 251 chars split in the index) cannot be kept exactly on decoded text, and neither touches a corpus file. - Values stay double. - Blocks that fail to compile are still recovered statement by statement, since our parser and MilkDrop's disagree on what an error is (_aboeq). Measured against main, last 10 of 60 frames: - control presets are identical with fidelity on and off; - of the 32 presets that read differently, 15 change with fidelity on (12 by more than 1%), and all are identical with it off; - the 900-preset sample keeps its classes. 9 new tests. The docs' test count follows. Refs #580, #560 --- README.md | 4 +- README.tr.md | 4 +- ROADMAP.md | 4 +- src/admin/milkdrop-panel.js | 5 +- src/shared/milkdrop.js | 249 ++++++++++++++++++++++++++- src/visualizer/modes/milkdrop.js | 33 +++- tests/milkdrop-fidelity-comp.test.js | 43 +++++ tests/milkdrop-md2-read.test.js | 138 +++++++++++++++ 8 files changed, 463 insertions(+), 17 deletions(-) create mode 100644 tests/milkdrop-md2-read.test.js diff --git a/README.md b/README.md index b59d125..8f0aec3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#build--distribution) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Tests](https://img.shields.io/badge/tests-2187%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2196%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1308,7 +1308,7 @@ npm test npm start -- --smoke ``` -**2187 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2196 unit tests, all passing.** They are written to check answers, not to exercise lines: - **Formulas** are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor diff --git a/README.tr.md b/README.tr.md index ce08d63..98569bd 100644 --- a/README.tr.md +++ b/README.tr.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#paketleme--dağıtım) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Test](https://img.shields.io/badge/test-2187%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2196%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1306,7 +1306,7 @@ npm test npm start -- --smoke ``` -**2187 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2196 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: - **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı diff --git a/ROADMAP.md b/ROADMAP.md index 69dfafa..e69070a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,9 +129,9 @@ npm test npm start -- --smoke ``` -- **2187 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2196 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; 105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at - that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 590 + that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 599 on `main` since. Formulas are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the diff --git a/src/admin/milkdrop-panel.js b/src/admin/milkdrop-panel.js index bf4fe29..0f51646 100644 --- a/src/admin/milkdrop-panel.js +++ b/src/admin/milkdrop-panel.js @@ -945,7 +945,8 @@ // Doğrulama: yüklü presetin derleme durumu if (md.source && window.SVMilkdrop) { try { - const p = new window.SVMilkdrop.Preset(md.source); + // Görselleştiricinin okuyacağı kuralla (#580: uyum açıkken MilkDrop'unki) + const p = new window.SVMilkdrop.Preset(md.source, { accurate: md.accurate !== false }); /* Sayı ve metin AYRI düğümlerde. i18n sözlüğü metin düğümlerini birebir eşleştiriyor, dolayısıyla '3 hata' gibi birleşik bir metin hiçbir zaman eşleşmez ve İngilizce arayüzde Türkçe kalırdı — @@ -1301,7 +1302,7 @@ const items = []; let bad = 0; for (const f of r.files) { - const parsed = M ? new M.Preset(f.text, { name: f.name }) : null; + const parsed = M ? new M.Preset(f.text, { name: f.name, accurate: md.accurate !== false }) : null; if (parsed && parsed.errors.length) bad++; items.push({ id: 'md_' + Math.random().toString(36).slice(2, 10), diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index f53f843..9f306e4 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -841,6 +841,237 @@ }; } + /* MILKDROP 2'NİN OKUYUŞU (#580). Uyum açıkken preset dosyası bununla + okunuyor (`readMilk`); kapalıyken yukarıdaki `parseMilk` duruyor. + + MilkDrop dosyayı önce satır satır bir dizine çeviriyor (state.cpp + _GetLineByName; BeatDrop'un D3D9 hâli aynı): satırın ADI ilk `=`ye, ilk + boşluğa ya da satır sonuna kadar olan kısım, DEĞERİ ondan sonrası. + Sonra anahtarları KENDİ sırasıyla arıyor (CState::Import, CWave::Import, + CShape::Import): önce bir önceki okumanın hemen ardındaki satıra + bakıyor, o değilse baştan tarıyor. Bizim ayrıştırıcımızdan farkları — + korpusun 10.332 presetinden 25'inde görülüyor: + - anahtar büyük/küçük harfe duyarlı (`PSVERSION_comp` okunmuyor); + - girintili satırın adı boş, hiç okunmuyor; `anahtar değer` (boşlukla) + okunuyor, `anahtar = değer` okunmuyor (değer `=` ile başlıyor); + - iki kez yazılmış anahtarda sıradaki ya da İLK geçiş, son değil; + - tam sayı anahtarları `%d`: `textured=0.05` 0, `bBrighten=0.5` 0; + kayan noktalılar `%f`: baştaki sayı, `.975;` 0,975; sayı yoksa + anahtar okunmamış sayılıyor; + - numaralı kod satırları ilk eksik numarada bitiyor, ilk satırdaki ters + tırnak her kod satırında atılıyor; + - denklem satırlarında `//` ve `\\` satır sonuna kadar yorum ve satırlar + ARADA HİÇBİR ŞEY OLMADAN yapışıyor, satır sonundaki boşluk korunarak + (ReadCode, StripLinefeedCharsAndComments). Bizim ayrıştırıcı + yapışık biçim ayrışmazsa satır sonuyla birleştiriyordu. + Okunmayan anahtar sonuçta YOK; varsayılanı motor veriyor. + + Yapılmayanlar: MilkDrop baytları okuyor, motor çözülmüş metni alıyor. + 0xFF baytını dosya sonu sayması ve 251 karakteri aşan değeri dizinde + ikinci bir satıra bölmesi bayt kuralları; çözülmüş metinde birebir + kurulamıyor ve korpusta hiçbir dosyada etkisi yok. Değerler double + kalıyor: MilkDrop float'a çeviriyor, fark 1e-7'nin altında. Derlenemeyen + bir blok MilkDrop'ta bütünüyle düşüyor, bizde deyim deyim kurtarılıyor: + bizim ayrıştırıcımızla MilkDrop'unki neyin hata olduğunda ayrışıyor + (korpusta `_aboeq` gibi iç işlevler), o yüzden bütünüyle düşürmek + yanlış blokları da düşürürdü. */ + function md2Index(text) { + const s = String(text == null ? '' : text); + const N = s.length; + const names = []; + const vals = []; + let i = 0; + while (i < N) { + // Ad: satır sonuna, boşluğa ya da '='ye kadar + let j = i; + while (j < N) { + const c = s.charCodeAt(j); + if (c === 13 || c === 10 || c === 32 || c === 61) break; + j++; + } + if (j >= N) break; + const stop = s.charCodeAt(j); + let next = j + 1; + if (stop === 61 || stop === 32) { + names.push(s.slice(i, j)); + // Aramada okunan değer: satır sonuna kadar + let e = next; + while (e < N && s.charCodeAt(e) !== 13 && s.charCodeAt(e) !== 10) e++; + vals.push(s.slice(next, e)); + // Dizin satırın geri kalanını LF'ye kadar yutuyor (fgets) + const lf = s.indexOf('\n', next); + next = lf < 0 ? N : lf + 1; + } + // Arta kalan satır sonları + while (next < N && (s.charCodeAt(next) === 13 || s.charCodeAt(next) === 10)) next++; + i = next; + } + /* Arama: önce bir önceki okumanın ardındaki satır, o değilse baştan. + Bulunamayan anahtar sırayı bozmuyor. */ + let line = 0; + const find = (name) => { + if (!(line < names.length && names[line] === name)) { + const k = names.indexOf(name); + if (k < 0) return null; + line = k; + } + return vals[line++]; + }; + return { find }; + } + + // %d: baştaki boşluk, işaret ve rakamlar; %f: C yerel ayarında baştaki sayı + const md2Int = (v) => { + const m = /^[ \t\n\v\f\r]*([+-]?\d+)/.exec(v); + return m ? parseInt(m[1], 10) : null; + }; + const md2Float = (v) => { + const m = /^[ \t\n\v\f\r]*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/.exec(v); + return m ? parseFloat(m[1]) : null; + }; + + /* Sürüm satırları, MilkDrop'un Import'ta okuduğu sırayla ve kuralıyla — + dosyanın ilk okumaları bunlar, yani `parseMilkMd2` de tam bu değerleri + buluyor. Karışımlar (milkdrop-mashup.js) aynı kuralı buradan alıyor. */ + function readVersions(text, idx) { + const ix = idx || md2Index(text); + const out = {}; + const I = (k) => { + const v = ix.find(k); + const n = v === null ? null : md2Int(v); + if (n !== null) out[k.toLowerCase()] = n; + }; + I('MILKDROP_PRESET_VERSION'); + const ver = typeof out.milkdrop_preset_version === 'number' ? out.milkdrop_preset_version : 100; + if (ver === 200) I('PSVERSION'); + else if (ver > 200) { I('PSVERSION_WARP'); I('PSVERSION_COMP'); } + return out; + } + + // `seen` verilirse aranan her anahtar (küçük harfle) oraya yazılıyor + function parseMilkMd2(text, seen) { + const ix = md2Index(text); + const params = readVersions(text, ix); + const put = (k, parse) => { + if (seen) seen.add(k.toLowerCase()); + const v = ix.find(k); + const n = v === null ? null : parse(v); + if (n !== null) params[k.toLowerCase()] = n; + }; + const I = (...keys) => keys.forEach((k) => put(k, md2Int)); + const F = (...keys) => keys.forEach((k) => put(k, md2Float)); + const code = (prefix) => { + const out = []; + for (let n = 1; ; n++) { + const v = ix.find(prefix + n); + if (v === null) break; + out.push(v.charAt(0) === '`' ? v.slice(1) : v); + } + return out; + }; + // Denklem: satır satır yorum kesiliyor, satırlar yapışıyor + const eq = (lines) => lines.map((l) => { + const a = l.indexOf('//'); + const b = l.indexOf('\\\\'); + const cut = a < 0 ? b : b < 0 ? a : Math.min(a, b); + return cut < 0 ? l : l.slice(0, cut); + }).join(''); + + // Genel + F('fRating', 'fDecay', 'fGammaAdj', 'fVideoEchoZoom', 'fVideoEchoAlpha'); + I('nVideoEchoOrientation', 'bRedBlueStereo', 'bBrighten', 'bDarken', 'bSolarize', 'bInvert'); + F('fShader', 'b1n', 'b2n', 'b3n', 'b1x', 'b2x', 'b3x', 'b1ed'); + // Dalga + I('nWaveMode', 'bAdditiveWaves', 'bWaveDots', 'bWaveThick', 'bModWaveAlphaByVolume', 'bMaximizeWaveColor'); + F('fWaveAlpha', 'fWaveScale', 'fWaveSmoothing', 'fWaveParam', 'fModWaveAlphaStart', 'fModWaveAlphaEnd', + 'wave_r', 'wave_g', 'wave_b', 'wave_x', 'wave_y', + 'nMotionVectorsX', 'nMotionVectorsY', 'mv_dx', 'mv_dy', 'mv_l', 'mv_r', 'mv_g', 'mv_b'); + I('bMotionVectorsOn'); + F('mv_a'); + const waves = []; + for (let i = 0; i < 4; i++) { + const k = (n) => 'wavecode_' + i + '_' + n; + I(k('enabled'), k('samples'), k('sep'), k('bSpectrum'), k('bUseDots'), k('bDrawThick'), k('bAdditive')); + F(k('scaling'), k('smoothing'), k('r'), k('g'), k('b'), k('a')); + const w = { index: i }; + const init = code('wave_' + i + '_init'); + const frame = code('wave_' + i + '_per_frame'); + const point = code('wave_' + i + '_per_point'); + if (init.length) w.init = eq(init); + if (frame.length) w.per_frame = eq(frame); + if (point.length) w.per_point = eq(point); + if (init.length || frame.length || point.length) waves.push(w); + } + const shapes = []; + for (let i = 0; i < 4; i++) { + const k = (n) => 'shapecode_' + i + '_' + n; + I(k('enabled'), k('sides'), k('additive'), k('thickOutline'), k('textured'), k('num_inst')); + F(k('x'), k('y'), k('rad'), k('ang'), k('tex_ang'), k('tex_zoom'), k('r'), k('g'), k('b'), k('a'), + k('r2'), k('g2'), k('b2'), k('a2'), k('border_r'), k('border_g'), k('border_b'), k('border_a')); + const s = { index: i }; + const init = code('shape_' + i + '_init'); + const frame = code('shape_' + i + '_per_frame'); + if (init.length) s.init = eq(init); + if (frame.length) s.per_frame = eq(frame); + if (init.length || frame.length) shapes.push(s); + } + // Hareket + F('zoom', 'rot', 'cx', 'cy', 'dx', 'dy', 'warp', 'sx', 'sy'); + I('bTexWrap', 'bDarkenCenter'); + F('fWarpAnimSpeed', 'fWarpScale', 'fZoomExponent', + 'ob_size', 'ob_r', 'ob_g', 'ob_b', 'ob_a', 'ib_size', 'ib_r', 'ib_g', 'ib_b', 'ib_a'); + const init = eq(code('per_frame_init_')); + const perFrame = eq(code('per_frame_')); + const perPixel = eq(code('per_pixel_')); + const warpShader = code('warp_').join('\n'); + const compShader = code('comp_').join('\n'); + return { params, init, perFrame, perPixel, warpShader, compShader, waves, shapes }; + } + + // Uyum anahtarına göre okuyuş: açıkken MilkDrop'unki, kapalıyken eski ayrıştırıcı + const readMilk = (text, accurate) => (accurate === false ? parseMilk(text) : parseMilkMd2(text)); + + /* İki okuyuş bu dosyada MOTORUN KULLANDIĞI bir şeyde ayrışıyor mu. Uyum + anahtarı çevrilince motor presetini ancak o zaman yeniden kuruyor + (denklem durumu baştan başlıyor); ayrışmıyorsa preset yerinde kalıyor. + Karşılaştırılan: + - aşama sürümleri, motorun çıkardığı hâliyle (`md2Versions`) — ham + `PSVERSION` satırı değil: MilkDrop onu sürüm 200 değilse okumuyor + ve dosyaların çoğu yine de yazıyor; + - motorun dosyadan okuduğu sayısal anahtarlar: MilkDrop'un anahtarları + ve kare değişkeni adları (eski ayrıştırıcı `decay=` gibi bir başlık + satırını da okuyordu); + - denklemler boşluksuz, shader'lar satır sonları kırpılarak: MilkDrop + satır sonundaki boşluğu koruyor, eski ayrıştırıcı kırpıyordu. + Boşluksuz karşılaştırma, satır sonundaki bir boşluğun iki simgeyi + ayırdığı durumu kaçırabilir; o preset bir sonraki yüklenişinde doğru + okunuyor, yalnız anahtar çevrildiği an eski okuyuşla kalıyor. */ + const MD2_READ_KEYS = new Set(); + function readingsDiffer(text) { + const a = parseMilk(text); + const b = parseMilkMd2(text); + if (!MD2_READ_KEYS.size) { + // MilkDrop'un aradığı bütün anahtarlar (boş bir dosyada da hepsi aranıyor) + parseMilkMd2('', MD2_READ_KEYS); + for (const k of PF_RESET) MD2_READ_KEYS.add(k); + } + const eqs = (f) => JSON.stringify([f.init, f.perFrame, f.perPixel, + (f.waves || []).map((w) => [w.index, w.init, w.per_frame, w.per_point]), + (f.shapes || []).map((s) => [s.index, s.init, s.per_frame])].map(function strip(x) { + return Array.isArray(x) ? x.map(strip) : typeof x === 'string' ? x.replace(/\s+/g, '') : x; + })); + const shaders = (f) => [f.warpShader, f.compShader].map((s) => String(s || '').replace(/[ \t]+$/gm, '')).join('\u0000'); + if (eqs(a) !== eqs(b) || shaders(a) !== shaders(b)) return true; + if (JSON.stringify(md2Versions(a.params)) !== JSON.stringify(md2Versions(b.params))) return true; + // Sürüm anahtarları bu kümede yok (readVersions kaydetmiyor): yukarıda karşılaştırıldılar + for (const k of MD2_READ_KEYS) { + const va = typeof a.params[k] === 'number' ? a.params[k] : undefined; + const vb = typeof b.params[k] === 'number' ? b.params[k] : undefined; + if (va !== vb) return true; + } + return false; + } + /* Bir presetin çalıştırılabilir hali. Preset yüklendiğinde blokları derler, kare başına per_frame'i bir kez, @@ -1071,14 +1302,20 @@ class Preset { constructor(text, opts) { const o = opts || {}; - this.file = parseMilk(text); - this.pool = new Pool(); - this.errors = []; - this.name = o.name || this.file.params.psetname || ''; /* "MilkDrop uyumu" anahtarı. Görselleştirici her kare kendi ayarını buraya yazıyor; alt blokların hangi kare değişkenlerini gördüğünü - seçiyor (bkz. SHARED_LEGACY). */ + seçiyor (bkz. SHARED_LEGACY). Dosyanın hangi kuralla OKUNDUĞUNU da + o seçiyor (#580, `readMilk`) — okuyuş kurulumda bir kez yapılıyor ve + `readAcc`ta kalıyor; anahtar sonradan çevrilirse görselleştirici + iki okuyuş ayrışıyorsa presetini yeniden kuruyor. */ this.accurate = o.accurate !== false; + this.readAcc = this.accurate; + this.file = readMilk(text, this.accurate); + this.pool = new Pool(); + this.errors = []; + // `psetname` MilkDrop'un okuduğu bir anahtar değil; ad için metinden + const pname = /^[ \t]*psetname[ \t]*=(.*)$/im.exec(String(text == null ? '' : text)); + this.name = o.name || (pname ? pname[1].trim() : '') || ''; /* MilkDrop varsayılanları. Dosya bunları belirtmeyebilir ve havuzun doğal başlangıcı 0; kırpma sonrası 0 SİYAH demek olurdu. MilkDrop'ta @@ -1720,7 +1957,7 @@ const api = { tokenize, parse, compile, Pool, FUNCS, parseMilk, Preset, clampColor, colorNorm, md2Versions, stagePlan, genWarpText, genCompText, - echoFlipBits, fixedCompWeights }; + echoFlipBits, fixedCompWeights, parseMilkMd2, readMilk, readVersions, readingsDiffer }; if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof window !== 'undefined') window.SVMilkdrop = api; })(); diff --git a/src/visualizer/modes/milkdrop.js b/src/visualizer/modes/milkdrop.js index 08457d7..f4de499 100644 --- a/src/visualizer/modes/milkdrop.js +++ b/src/visualizer/modes/milkdrop.js @@ -2099,7 +2099,10 @@ void main(){ /* Kaynak saklanıyor: bağlam kaybından sonra (#572) shader'lar ondan yeniden derleniyor, preset nesnesine dokunmadan. */ this._presetSrc = src; - this.preset = new M.Preset(src, { seed: 1234 }); + /* Dosya uyum anahtarına göre okunuyor (#580): açıkken MilkDrop'un + okuyuşuyla (`readMilk`), kapalıyken eski ayrıştırıcıyla. Varsayılana + bırakılmıyor — Preset'in varsayılanı açık. */ + this.preset = new M.Preset(src, { seed: 1234, accurate: this._wantAcc !== false }); this.error = this.preset.errors.join(' | '); this.frameNo = 0; this.presetTime = 0; @@ -2175,6 +2178,28 @@ void main(){ /* Geçişi bitirir ve eski presetin programlarını serbest bırakır. Programlar burada siliniyor, `_buildPresetShaders`ta değil: orası yalnız YENİ yuvaya bakıyor ve eski yuva geçiş boyunca çiziliyor. */ + /* Uyum anahtarı dosyanın hangi kuralla OKUNDUĞUNU da seçiyor (#580): + açıkken MilkDrop'un okuyuşu, kapalıyken eski ayrıştırıcı. Okuyuş + preset kurulurken yapılıyor (`readAcc`). Anahtar çevrildiğinde iki + okuyuş bu dosyada ayrışıyorsa preset yeniden kuruluyor — denklem + durumu baştan başlıyor, sürüyorsa geçiş bırakılıyor ve aşamalar da + yeniden kuruluyor; ayrışmıyorsa yalnız işareti güncelleniyor. Önceden + kurulmuş bir preset de burada yakalanıyor. Dönüş: yeniden kuruldu mu. */ + _syncReading() { + const wantAcc = this._wantAcc !== false; + if (!this._presetSrc || !this.preset || this.preset.readAcc === wantAcc) return false; + const M = window.SVMilkdrop; + if (M.readingsDiffer && M.readingsDiffer(this._presetSrc)) { + this._dropOld(); + this.preset = new M.Preset(this._presetSrc, { seed: 1234, accurate: wantAcc }); + this.error = this.preset.errors.join(' | '); + this._stagesAcc = null; + return true; + } + this.preset.readAcc = wantAcc; + return false; + } + _dropOld() { const gl = this.gl; if (gl) { @@ -2230,7 +2255,8 @@ void main(){ const acc = this._wantAcc !== false; const job = { warp: null, comp: null, acc }; if (!this.gl || !T || !M) return job; - const parsed = M.parseMilk(src); + // Aşamalar da presetin okunduğu kuralla okunuyor (#580) + const parsed = M.readMilk ? M.readMilk(src, acc) : M.parseMilk(src); let fl = parsed; if (acc && M.stagePlan) { const plan = M.stagePlan(parsed); @@ -3097,8 +3123,9 @@ void main(){ havuzu, q değişkenleri ve saat kaybın olduğu yerden sürüyor. Preset bu arada zaten değiştiyse `_buildPresetShaders` bayrağı orada düşürüyor ve burada ikinci kez derlenmiyor. */ + this._syncReading(); /* Uyum anahtarı çevrildiyse de: aşamaları hangi kuralın seçtiği ona - bağlı (#580). Preset nesnesi burada da aynı kalıyor. */ + bağlı (#580). */ if ((this._shadersLost || this._stagesAcc !== (this._wantAcc !== false)) && this._presetSrc) { this._buildPresetShaders(this._presetSrc); } diff --git a/tests/milkdrop-fidelity-comp.test.js b/tests/milkdrop-fidelity-comp.test.js index eaf921a..a21c6ce 100644 --- a/tests/milkdrop-fidelity-comp.test.js +++ b/tests/milkdrop-fidelity-comp.test.js @@ -284,6 +284,8 @@ test('kendi presetlerimiz yalnız MilkDrop\'un okuduğu anahtarları, birer kez const sorted = nums.slice().sort((a, b) => a - b); sorted.forEach((n, k) => assert.strictEqual(n, k + 1, id + ': ' + prefix + ' numarasında boşluk')); } + // İki okuyuş kendi presetlerimizde aynı: uyum anahtarı onları değiştirmiyor + assert.strictEqual(M.readingsDiffer(text), false, id + ': iki okuyuş ayrışıyor'); } assert.ok(texts.length === 5 + 64 + 30 && checked > 10000, 'yeterince satır denetlendi: ' + checked); }); @@ -332,6 +334,47 @@ test('motor: uyum açıkken aşamayı sürüm seçiyor, kapalıyken metin', () = assert.ok(g.comp && /ret \*= 2\.00;/.test(g.comp.text), 'üretilen birleştirme'); }); +/* Uyum anahtarı dosyanın OKUNUŞUNU da seçiyor (#580): çevrildiğinde iki + okuyuş bu dosyada ayrışıyorsa preset yeniden kuruluyor, ayrışmıyorsa + yalnız işareti güncelleniyor. Önceden (anahtar çevrilmeden) kurulmuş bir + preset de yakalanıyor, sürüyorsa geçiş bırakılıyor; aşamalar da o + kuralla okunuyor. */ +test('motor: anahtar çevrilince okuyuşu ayrışan preset yeniden kuruluyor', () => { + const { m, M: MM } = engine(); + const diff = 'fdecay=0.5\nper_frame_1=zoom = 1.01;\n'; + const same = 'fDecay=0.5\nper_frame_1=zoom = 1.01;\n'; + m._wantAcc = true; + m._presetSrc = diff; + m.preset = new MM.Preset(diff, { seed: 1234, accurate: true }); + const before = m.preset; + m._wantAcc = false; + assert.strictEqual(m._syncReading(), true); + assert.notStrictEqual(m.preset, before, 'yeniden kuruldu'); + assert.strictEqual(m.preset.readAcc, false); + assert.strictEqual(m.preset.file.params.fdecay, 0.5, 'eski ayrıştırıcıyla okundu'); + assert.strictEqual(m._syncReading(), false, 'kural aynıyken dokunulmuyor'); + // Ayrışmayan dosya: preset yerinde, yalnız işaret + m._presetSrc = same; + m.preset = new MM.Preset(same, { seed: 1234, accurate: true }); + const kept = m.preset; + assert.strictEqual(m._syncReading(), false); + assert.strictEqual(m.preset, kept); + assert.strictEqual(kept.readAcc, false); + // Geçiş sürerken yeniden kurulursa geçiş bırakılıyor + m._presetSrc = diff; + m.preset = new MM.Preset(diff, { seed: 1234, accurate: false }); + m.oldPreset = new MM.Preset(same, { seed: 1234, accurate: false }); + m._wantAcc = true; + assert.strictEqual(m._syncReading(), true); + assert.strictEqual(m.oldPreset, null); + assert.strictEqual(m.preset.file.params.fdecay, undefined, 'MilkDrop\'un okuyuşuyla'); + // Kurulum ve aşamalar kullanıcının kuralıyla + const code = bare(read('src/visualizer/modes/milkdrop.js')); + assert.match(code, /this\.preset = new M\.Preset\(src, \{ seed: 1234, accurate: this\._wantAcc !== false \}\);/); + assert.match(code, /const parsed = M\.readMilk \? M\.readMilk\(src, acc\) : M\.parseMilk\(src\);/); + assert.match(code, /this\._syncReading\(\);\s*if \(\(this\._shadersLost \|\| this\._stagesAcc !== \(this\._wantAcc !== false\)\) && this\._presetSrc\)/); +}); + test('motor: geçişte yönü farklı iki yankı sönüp yeniden beliriyor', () => { const { m, M: MM } = engine(); m._wantAcc = true; diff --git a/tests/milkdrop-md2-read.test.js b/tests/milkdrop-md2-read.test.js new file mode 100644 index 0000000..37fe361 --- /dev/null +++ b/tests/milkdrop-md2-read.test.js @@ -0,0 +1,138 @@ +'use strict'; +/* MILKDROP 2'NİN DOSYA OKUYUŞU (#580). + * + * Uyum açıkken preset dosyası MilkDrop'un okuduğu gibi okunuyor + * (shared/milkdrop.js `parseMilkMd2`, `readMilk`); kapalıyken eski + * ayrıştırıcı. Kurallar Nullsoft'un kodundan (jecassis/foo_vis_milk2 + * 5b44cea, state.cpp _GetLineByName, GetFastInt/Float/String, ReadCode, + * CState::Import; BeatDrop'un D3D9 hâli aynı): + * - satırın adı ilk `=`ye, boşluğa ya da satır sonuna kadar; + * - anahtar büyük/küçük harfe duyarlı, MilkDrop'un sırasıyla aranıyor: + * önce bir önceki okumanın ardındaki satır, o değilse ilk geçiş; + * - tam sayılar `%d`, kayan noktalılar `%f`; + * - numaralı kod ilk eksik numarada bitiyor, denklem satırları yorumları + * (`//`, `\\`) atılarak ARAYA HİÇBİR ŞEY KONMADAN yapışıyor. + * Korpusun 10.332 presetinden 32'sinde iki okuyuş motorun kullandığı bir + * şeyde ayrışıyor. */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +const M = require('../src/shared/milkdrop.js'); +const ROOT = path.join(__dirname, '..'); + +test('anahtar büyük/küçük harfe duyarlı; girintili satır ve "anahtar = değer" okunmuyor, "anahtar değer" okunuyor', () => { + const t = 'MILKDROP_PRESET_VERSION=201\nPSVERSION_comp=3\nfdecay=0.5\n fGammaAdj=3\nzoom 1.02\nrot = 0.1\n'; + const p = M.parseMilkMd2(t).params; + assert.strictEqual(p.milkdrop_preset_version, 201); + assert.strictEqual(p.psversion_comp, undefined, 'PSVERSION_comp okunmuyor'); + assert.strictEqual(p.fdecay, undefined, 'fdecay okunmuyor'); + assert.strictEqual(p.fgammaadj, undefined, 'girintili satırın adı boş'); + assert.strictEqual(p.zoom, 1.02, 'boşluk da ayırıcı'); + assert.strictEqual(p.rot, undefined, 'değer "= 0.1": sayı yok'); + // Eski ayrıştırıcı hepsini okuyordu + const old = M.parseMilk(t).params; + assert.strictEqual(old.psversion_comp, 3); + assert.strictEqual(old.fdecay, 0.5); + assert.strictEqual(old.fgammaadj, 3); +}); + +test('iki kez yazılmış anahtar: önce bir önceki okumanın ardındaki satır, değilse ilk geçiş', () => { + // fRating okunduktan sonraki satır fDecay: o okunuyor, ilk geçiş değil + assert.strictEqual(M.parseMilkMd2('fDecay=0.5\nfRating=3\nfDecay=0.7\n').params.fdecay, 0.7); + // Ardındaki satır başka bir anahtar: baştan tarama, ilk geçiş + assert.strictEqual(M.parseMilkMd2('fDecay=0.5\nfGammaAdj=2\nfDecay=0.7\n').params.fdecay, 0.5); + assert.strictEqual(M.parseMilkMd2('fGammaAdj=2\nfDecay=0.5\nfDecay=0.7\n').params.fdecay, 0.5, 'tarama ilkini buluyor'); + assert.strictEqual(M.parseMilk('fDecay=0.5\nfGammaAdj=2\nfDecay=0.7\n').params.fdecay, 0.7, 'eski: sonuncusu'); + // Korpustaki biçim: şekil açık yazılmış, sonra kapalı + assert.strictEqual(M.parseMilkMd2('shapecode_2_enabled=1\nshapecode_2_enabled=0\n').params.shapecode_2_enabled, 1); +}); + +test('tam sayılar %d, kayan noktalılar %f; sayı yoksa anahtar okunmamış', () => { + const p = M.parseMilkMd2('bBrighten=0.5\nnWaveMode=3.9\nshapecode_0_textured=0.05\nfDecay=.975;\n' + + 'fGammaAdj=abc\nzoom=1e-1x\nwarp=-.5\nnVideoEchoOrientation=-1.7\n').params; + assert.strictEqual(p.bbrighten, 0, 'bBrighten=0.5 kapalı'); + assert.strictEqual(p.nwavemode, 3); + assert.strictEqual(p.shapecode_0_textured, 0); + assert.strictEqual(p.fdecay, 0.975, 'baştaki sayı'); + assert.strictEqual(p.fgammaadj, undefined); + assert.strictEqual(p.zoom, 0.1); + assert.strictEqual(p.warp, -0.5); + assert.strictEqual(p.nvideoechoorientation, -1); +}); + +test('numaralı kod ilk eksik numarada bitiyor; ters tırnak atılıyor; // ve \\\\ yorum; satırlar yapışıyor', () => { + const t = 'per_frame_1=a = 1; // yorum\nper_frame_2=b = 2; \\\\ ters yorum\nper_frame_3=`c = 3; \n' + + 'per_frame_5=d = 4;\nwarp_1=`shader_body\nwarp_2=`{ ret = 0; }\ncomp_2=ret = 1;\n'; + const f = M.parseMilkMd2(t); + assert.strictEqual(f.perFrame, 'a = 1; b = 2; c = 3; ', 'satır sonu boşluğu korunuyor, 5 okunmuyor'); + assert.strictEqual(f.warpShader, 'shader_body\n{ ret = 0; }'); + assert.strictEqual(f.compShader, '', 'comp_1 yok: hiç okunmuyor'); + const old = M.parseMilk(t); + assert.ok(/d = 4/.test(old.perFrame), 'eski: boşluktan sonrası da'); + // Ayraçsız yapışma: MilkDrop satır sonu koymuyor + assert.strictEqual(M.parseMilkMd2('per_frame_1=a = 1\nper_frame_2=b = 2;\n').perFrame, 'a = 1b = 2;'); + // Dalga ve şekil blokları numaralarıyla, 0..3 + const w = M.parseMilkMd2('wave_2_per_point1=x = sample;\nshape_1_per_frame1=rad = 0.2;\nshape_4_per_frame1=rad = 0.3;\n'); + assert.deepStrictEqual(w.waves, [{ index: 2, per_point: 'x = sample;' }]); + assert.deepStrictEqual(w.shapes, [{ index: 1, per_frame: 'rad = 0.2;' }], 'MilkDrop 2 dört şekil okuyor'); +}); + +test('sürüm satırları MilkDrop\'un sırasıyla: 200 PSVERSION, üstü WARP/COMP', () => { + assert.deepStrictEqual(M.readVersions('MILKDROP_PRESET_VERSION=200\nPSVERSION=3\nPSVERSION_WARP=0\n'), + { milkdrop_preset_version: 200, psversion: 3 }); + assert.deepStrictEqual(M.readVersions('MILKDROP_PRESET_VERSION=201\nPSVERSION=3\nPSVERSION_WARP=0\nPSVERSION_COMP=2\n'), + { milkdrop_preset_version: 201, psversion_warp: 0, psversion_comp: 2 }); + assert.deepStrictEqual(M.readVersions('milkdrop_preset_version=201\nPSVERSION_WARP=2\n'), {}, 'küçük harf: MilkDrop 1'); + assert.deepStrictEqual(M.readVersions('MILKDROP_PRESET_VERSION=201\nPSVERSION_WARP=2\nPSVERSION_WARP=0\n'), + { milkdrop_preset_version: 201, psversion_warp: 2 }, 'ardındaki satır: ilk yazılan'); + // Motor aşamayı bu değerlerden seçiyor + assert.strictEqual(M.stagePlan(M.parseMilkMd2('MILKDROP_PRESET_VERSION=201\nPSVERSION_comp=0\ncomp_1=`ret = 1;\n')).comp, 'shader', + 'PSVERSION_comp okunmuyor: 2'); +}); + +test('readMilk ve Preset: uyum kapalıyken eski ayrıştırıcı; okuyuş kurulumda, readAcc\'ta', () => { + const t = 'fdecay=0.5\n'; + assert.strictEqual(M.readMilk(t, false).params.fdecay, 0.5); + assert.strictEqual(M.readMilk(t, true).params.fdecay, undefined); + const a = new M.Preset(t, { seed: 1 }); + assert.strictEqual(a.readAcc, true); + a.frame({ time: 0, frame: 0 }); + assert.strictEqual(a.get('decay'), 0.98, 'MilkDrop okumuyor: varsayılan'); + const b = new M.Preset(t, { seed: 1, accurate: false }); + assert.strictEqual(b.readAcc, false); + b.frame({ time: 0, frame: 0 }); + assert.strictEqual(b.get('decay'), 0.5); + // `psetname` MilkDrop'un anahtarı değil ama ad yine metinden + assert.strictEqual(new M.Preset('psetname=Deneme Adı\n').name, 'Deneme Adı'); +}); + +test('readingsDiffer: motorun kullandığı bir şeyde ayrışan dosyada true, yalnız biçimde ayrışanda false', () => { + const md2 = 'MILKDROP_PRESET_VERSION=201\nPSVERSION=2\nPSVERSION_WARP=2\nPSVERSION_COMP=2\nfDecay=0.9\n'; + assert.strictEqual(M.readingsDiffer(md2), false, 'MilkDrop\'un okumadığı PSVERSION satırı fark sayılmıyor'); + assert.strictEqual(M.readingsDiffer('per_frame_1=a = 1; \nper_frame_2=b = 2;\n'), false, 'yalnız boşluk'); + assert.strictEqual(M.readingsDiffer('fdecay=0.9\n'), true); + assert.strictEqual(M.readingsDiffer('shapecode_0_textured=0.5\n'), true); + assert.strictEqual(M.readingsDiffer('gamma=1\n'), true, 'başlıkta kare değişkeni adı'); + assert.strictEqual(M.readingsDiffer('per_frame_1=a = 1;\nper_frame_3=b = 2;\n'), true, 'numarada boşluk'); +}); + +/* Preset kuran her yer okuma kuralını AÇIKÇA veriyor: Preset'in varsayılanı + açık, görselleştirici ise kullanıcının ayarını izliyor. Verilmeyen bir + yer uyum kapalıyken MilkDrop'un okuyuşunu sessizce kullanırdı. */ +test('kaynakta her Preset kurulumu okuma kuralını veriyor', () => { + const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => + (e.isDirectory() ? walk(path.join(d, e.name)) : e.name.endsWith('.js') ? [path.join(d, e.name)] : [])); + let found = 0; + for (const file of walk(path.join(ROOT, 'src'))) { + const src = fs.readFileSync(file, 'utf-8'); + const re = /new [A-Za-z_.]*Preset\(([^;]*?)\)[;\n ]/g; + let m; + while ((m = re.exec(src)) !== null) { + found++; + assert.match(m[1], /accurate:/, path.relative(ROOT, file) + ': ' + m[0].slice(0, 80)); + } + } + assert.ok(found >= 4, 'Preset kurulumları bulunamadı: ' + found); +}); From 0ab71d0ed49be04d2976de3fe031aa924b62d6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 18:26:24 +0300 Subject: [PATCH 2/3] fix(milkdrop): mash-ups read their donors the way MilkDrop does A preset gives a mash-up a part only if MilkDrop reads that part from it: the part test now reads the file with the engine's MilkDrop reader (parseMilkMd2) and a shader counts only if the engine's stage rule draws it. The version lines written for a donated shader are the ones MilkDrop reads from the donor (readVersions): a donor writing PSVERSION_comp=3 gives version 2, and in a doubled PSVERSION_WARP the line MilkDrop finds. The same recipe can give different version lines than before, so the rule version goes to 3 and older saves keep their files. Without the engine loaded the module falls back to its old text tests. On the whole corpus the part test agrees with the engine for every preset and part (0.04 ms a test, was 0.014), a mash-up of one preset reads as that preset for all 10,332, and 5,000 random mash-ups keep every part and stage of their donors. Refs #579, #580 --- README.md | 4 +- README.tr.md | 4 +- ROADMAP.md | 4 +- src/shared/milkdrop-mashup.js | 78 ++++++++++++++++++++--------- tests/milkdrop-mashup-panel.test.js | 2 +- tests/milkdrop-mashup.test.js | 36 +++++++++++-- 6 files changed, 95 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 8f0aec3..76f09a0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#build--distribution) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Tests](https://img.shields.io/badge/tests-2196%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2197%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1308,7 +1308,7 @@ npm test npm start -- --smoke ``` -**2196 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2197 unit tests, all passing.** They are written to check answers, not to exercise lines: - **Formulas** are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor diff --git a/README.tr.md b/README.tr.md index 98569bd..433e56e 100644 --- a/README.tr.md +++ b/README.tr.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#paketleme--dağıtım) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Test](https://img.shields.io/badge/test-2196%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2197%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1306,7 +1306,7 @@ npm test npm start -- --smoke ``` -**2196 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2197 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: - **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı diff --git a/ROADMAP.md b/ROADMAP.md index e69070a..0863336 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,9 +129,9 @@ npm test npm start -- --smoke ``` -- **2196 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2197 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; 105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at - that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 599 + that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 600 on `main` since. Formulas are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the diff --git a/src/shared/milkdrop-mashup.js b/src/shared/milkdrop-mashup.js index 31fe704..5fd1cfa 100644 --- a/src/shared/milkdrop-mashup.js +++ b/src/shared/milkdrop-mashup.js @@ -39,8 +39,11 @@ 1: ilk kurallar (#579). 2: shader parçası MilkDrop'un aşama kuralına uyuyor (#580) — sürümü MilkDrop'ta 0 okunan bir shader karışımda da 0 yazılıyor; 1'de 2 - yazılıyor ve motor o shader'ı çiziyordu. */ - const VERSION = 2; + yazılıyor ve motor o shader'ı çiziyordu. + 3: sürüm satırları ve parça sınamaları MilkDrop'un okuyuşuyla (#580) — + büyük/küçük harfe duyarlı, iki kez yazılmışsa MilkDrop'un bulduğu: + `PSVERSION_comp=3` yazan bir vericinin shader'ı 3 değil 2 yazılıyor. */ + const VERSION = 3; const SLOTS = ['look', 'motion', 'waves', 'shapes', 'warp', 'comp']; // Shader parçası "yok" olabilir: görünümün sabit yolu (yankı, gama, bayraklar) o zaman çalışıyor const NONE = ''; @@ -67,10 +70,22 @@ return 'look'; } - /* Sürüm satırının değeri, ayrıştırıcının kuralıyla: değerin TAMAMI - sayıysa sayı, değilse "yok" (NaN; sürüm kuralı onu varsayılana - düşürüyor). Aynı anahtar iki kez geçerse sonuncusu — ayrıştırıcı da - öyle tutuyor. */ + /* Sürüm satırları MOTORUN okuduğu gibi (#580): `readVersions`, + MilkDrop'un Import'unun ilk okumaları — büyük/küçük harfe duyarlı, + iki kez yazılmışsa MilkDrop'un bulduğu, değer `%d`. Motor uyum açıkken + dosyayı bu kuralla okuyor; karışımın aşama kararı onunla aynı olmalı. + Motor yüklü değilse (bu modül tek başına) eski tarama: büyük/küçük + harfe duyarsız, sonuncusu, değerin tamamı sayıysa. */ + let ENGINE = null; + const engine = () => { + if (ENGINE) return ENGINE; + if (typeof window !== 'undefined' && window.SVMilkdrop && window.SVMilkdrop.readVersions) { + ENGINE = window.SVMilkdrop; + } else if (typeof module !== 'undefined' && typeof require === 'function') { + try { ENGINE = require('./milkdrop.js'); } catch (e) { ENGINE = null; } + } + return ENGINE; + }; const numVal = (raw) => { const n = parseFloat(raw); return isFinite(n) && /^[\s\-+.0-9eE]+$/.test(raw) ? n : NaN; @@ -78,6 +93,8 @@ // Yalnız sürüm satırları, metni ayrıştırmadan (aday sınaması için) function versionsOf(text) { + const E = engine(); + if (E && E.readVersions) return E.readVersions(String(text == null ? '' : text)); const ver = {}; const re = /^[ \t]*(milkdrop_preset_version|psversion_warp|psversion_comp|psversion)[ \t]*=(.*)$/gmi; let m; @@ -90,7 +107,6 @@ sürüm satırlarının değerleri. */ function split(text) { const out = { look: [], motion: [], waves: [], shapes: [], warp: [], comp: [] }; - const ver = {}; for (const raw of String(text == null ? '' : text).split(/\r?\n/)) { const line = raw.trim(); if (!line || line[0] === '[') continue; @@ -98,32 +114,48 @@ if (eq < 0) continue; const key = line.slice(0, eq).trim().toLowerCase(); const slot = slotOf(key); - if (slot === 'version') { - ver[key] = numVal(line.slice(eq + 1)); - continue; - } + // Sürüm satırları kopyalanmıyor, karışımda yeniden yazılıyor + if (slot === 'version') continue; /* Satırın iki ucu kırpılmış hâli: ayrıştırıcı da satırı öyle okuyor. Shader girintisi ters tırnaktan SONRA, kırpmadan etkilenmiyor. */ out[slot].push(line); } - return { lines: out, ver }; + return { lines: out, ver: versionsOf(text) }; } - /* Bir parçası var mı — ayrıştırıcıyla aynı cevap, ama ayrıştırmadan. - Aday seçimi on bin presetlik bir listede satır satır arıyor; her aday - için tam ayrıştırma pahalı olurdu. Ölçüldü: korpusun 10.332 presetinde - altı parçanın hepsi için ayrıştırıcıyla birebir aynı (ROADMAP #579). + /* Bir parçası var mı — motorun uyum açıkken DOSYAYI OKUDUĞU GİBİ (#580): + MilkDrop'un okuyuşu (`parseMilkMd2`), büyük/küçük harfe duyarlı, + numaralı kod ilk eksik numarada bitiyor, iki kez yazılmış anahtarda + MilkDrop'un bulduğu. Karışım satırları olduğu gibi kopyaladığı için + MilkDrop'un vericide okumadığı bir satır karışımda da okunmuyor; o + presetin o parçası yok sayılmalı. - motion — boş olmayan bir denklem satırı (yorum dışında) + motion — yorumlar atıldıktan sonra boş olmayan bir denklem bloğu waves/shapes — açık (enabled sıfırdan farklı) bir blok - warp/comp — boş olmayan bir shader satırı VE o aşamanın sürümü - sıfırdan büyük: MilkDrop sürümü 0 olan aşamanın metnini okumuyor, - motor da (#580); öyle bir preset o parçayı veremez */ + warp/comp — motorun aşama kuralında o aşama shader'la çiziliyor: + sürümü sıfırdan büyük ve metni boş değil + + Okuma aday başına ~0,2 ms (korpusta ölçüldü) ve son metin önbellekte: + bir presetin altı parçası tek okuma. Motor yüklü değilse (bu modül + tek başına) eski metin sınamaları. */ + let lastText = null; + let lastRead = null; function has(text, slot) { - if (slot === 'warp' || slot === 'comp') { - return hasLine(text, slot) && stageVersion(versionsOf(String(text == null ? '' : text)), slot) > 0; + if (slot === 'look') return true; + const s = String(text == null ? '' : text); + const E = engine(); + if (!E || !E.parseMilkMd2) { + if (slot === 'warp' || slot === 'comp') return hasLine(s, slot) && stageVersion(versionsOf(s), slot) > 0; + return hasLine(s, slot); + } + if (s !== lastText) { lastText = s; lastRead = E.parseMilkMd2(s); } + const f = lastRead; + if (slot === 'motion') return !!(f.init.trim() || f.perFrame.trim() || f.perPixel.trim()); + if (slot === 'waves' || slot === 'shapes') { + const re = slot === 'waves' ? /^wavecode_\d+_enabled$/ : /^shapecode_\d+_enabled$/; + return Object.keys(f.params).some((k) => re.test(k) && f.params[k] !== 0); } - return hasLine(text, slot); + return E.stagePlan(f)[slot] === 'shader'; } function hasLine(text, slot) { diff --git a/tests/milkdrop-mashup-panel.test.js b/tests/milkdrop-mashup-panel.test.js index 0044e42..c7709b4 100644 --- a/tests/milkdrop-mashup-panel.test.js +++ b/tests/milkdrop-mashup-panel.test.js @@ -161,7 +161,7 @@ test('karışımın metni parçalarının presetlerinden, önizleme kaydedilmiyo GP.rollAll(); const { mix, mixLast } = GP.state(); assert.strictEqual(cfg.milkdrop.presetId, mixLast.id); - assert.match(mixLast.id, /^md_mix2_[0-9a-f]{16}$/); + assert.match(mixLast.id, /^md_mix3_[0-9a-f]{16}$/); assert.strictEqual(mixLast.id, X.idOf(mix)); assert.strictEqual(cfg.milkdrop.source, mixLast.source); const f = M0.parseMilk(mixLast.source); diff --git a/tests/milkdrop-mashup.test.js b/tests/milkdrop-mashup.test.js index 3aaad8e..31da878 100644 --- a/tests/milkdrop-mashup.test.js +++ b/tests/milkdrop-mashup.test.js @@ -176,9 +176,12 @@ test('"yok" shader parçası bulanıklık aralıklarını da götürüyor', () = // ------------------------------------------------------------- parça var mı -test('"parçası var mı" ayrıştırıcıyla aynı cevabı veriyor', () => { +/* Parça sınamaları motorun uyum açıkken dosyayı OKUDUĞU gibi: MilkDrop'un + okuyuşu (#580). Karışım satırları olduğu gibi kopyaladığı için MilkDrop'un + vericide okumadığı bir satır karışımda da okunmuyor. */ +test('"parçası var mı" motorun okuyuşuyla aynı cevabı veriyor', () => { const parsedHas = (t, slot) => { - const f = M.parseMilk(t); + const f = M.parseMilkMd2(t); const enabled = (pre) => Object.keys(f.params).some((k) => new RegExp('^' + pre + '_\\d+_enabled$').test(k) && typeof f.params[k] === 'number' && f.params[k] !== 0); if (slot === 'look') return true; @@ -219,6 +222,33 @@ test('"parçası var mı" ayrıştırıcıyla aynı cevabı veriyor', () => { assert.strictEqual(X.has(t, s), parsedHas(t, s), s + ': ' + JSON.stringify(t.slice(0, 60))); } } + /* MilkDrop'un okuyuşunun eski ayrıştırıcıdan ayrıldığı yerler açıkça: + büyük harfli anahtar okunmuyor, girintili satır okunmuyor, iki kez + yazılmış anahtarda ilki, numaralı kod 1'den başlamıyorsa hiç okunmuyor, + sürüm satırı yanlış harfle yazılmışsa MilkDrop 1. */ + assert.strictEqual(X.has('[preset00]\nWAVECODE_1_ENABLED=1\n', 'waves'), false); + assert.strictEqual(X.has('[preset00]\n PER_FRAME_1=zoom = 1.01;\n', 'motion'), false); + assert.strictEqual(X.has('[preset00]\n per_frame_1=zoom = 1.01;\n', 'motion'), false); + assert.strictEqual(X.has('[preset00]\nshapecode_2_enabled=1\nshapecode_2_enabled=0\n', 'shapes'), true); + assert.strictEqual(X.has('[preset00]\nper_frame_2=zoom = 1.01;\n', 'motion'), false); + assert.strictEqual(X.has('milkdrop_preset_version=201\n[preset00]\ncomp_1=`ret = 1;\n', 'comp'), false); + assert.strictEqual(X.has('MILKDROP_PRESET_VERSION=201\n[preset00]\ncomp_1=`ret = 1;\n', 'comp'), true); +}); + +/* Karışımın sürüm satırları vericinin MilkDrop'ta OKUNAN sürümünden + (#580): `PSVERSION_comp=3` yanlış harfle yazılmış, MilkDrop okumuyor ve + 2 sayıyor; iki kez yazılmış `PSVERSION_WARP`ta ardındaki satır. */ +test('karışımın sürüm satırları vericinin MilkDrop\'ta okunan sürümü', () => { + const ver = (t) => { + const o = M.parseMilkMd2(t).params; + return [o.milkdrop_preset_version, o.psversion_warp, o.psversion_comp]; + }; + const odd = 'MILKDROP_PRESET_VERSION=201\nPSVERSION_comp=3\n[preset00]\ncomp_1=`ret = 1;\n'; + const out = X.compose({ look: odd, motion: odd, waves: odd, shapes: odd, warp: X.NONE, comp: odd }); + assert.deepStrictEqual(ver(out), [201, 0, 2], out.slice(0, 120)); + const dup = 'MILKDROP_PRESET_VERSION=201\nPSVERSION_WARP=2\nPSVERSION_WARP=0\n[preset00]\nwarp_1=`shader_body\n'; + const out2 = X.compose({ look: dup, motion: dup, waves: dup, shapes: dup, warp: dup, comp: X.NONE }); + assert.deepStrictEqual(ver(out2), [201, 2, 0], out2.slice(0, 120)); }); // --------------------------------------------------------- kimlik ve ad @@ -226,7 +256,7 @@ test('"parçası var mı" ayrıştırıcıyla aynı cevabı veriyor', () => { test('kimlik tariften: aynı tarif aynı kimlik, depo adıyla uyumlu', () => { const r = { look: 'a', motion: 'b', waves: 'c', shapes: 'a', warp: '', comp: 'd' }; const id = X.idOf(r); - assert.match(id, /^md_mix2_[0-9a-f]{16}$/); + assert.match(id, /^md_mix3_[0-9a-f]{16}$/); assert.strictEqual(X.idOf(Object.assign({}, r)), id); assert.notStrictEqual(X.idOf(Object.assign({}, r, { warp: 'b' })), id); assert.notStrictEqual(X.idOf(Object.assign({}, r, { look: 'b', motion: 'a' })), id, 'parçanın yeri de kimlikte'); From 513643172e63d65e98176f0cb413e3e38515733c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 18:26:24 +0300 Subject: [PATCH 3/3] docs: preset files read the way MilkDrop reads them README (both languages): the new reading with fidelity on. ROADMAP #580: the reader, the mash-ups following it, the measurements, and what was left out on purpose (byte rules, float values, whole-block compile failure) with the new open items (_aboeq and the rest of MilkDrop's internal functions, uv orientation). The #579 entry notes the rule version 3 and the part test's new cost. Refs #580, #579 --- README.md | 5 +++ README.tr.md | 5 +++ ROADMAP.md | 93 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 76f09a0..dbbd388 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,11 @@ that asserts the bar profile has no step in it. equations (wave scale and smoothing, the volume fade, warp speed and scale) come from the file as they do there, so a wave scale of 0 flattens the wave (102 presets of the corpus). Rendered through the engine, every case matches MilkDrop's formula within 2/255. +- **Preset files are read the way MilkDrop reads them.** With fidelity on, keys are case-sensitive, + a key written twice takes the value MilkDrop finds (usually the first), numbered code ends at the + first missing number, integer settings drop their fractions, and equation lines are glued the way + MilkDrop glues them, `\\` comments included. 32 presets of the corpus read differently; with + fidelity off the old parser stays. - **Composite shaders get the hue colour MilkDrop gives them.** MilkDrop passes every composite shader four slowly drifting corner colours as `hue_shader`, whatever the preset's `fShader` says; `fShader` only scales them on the fixed pipeline. The engine applied `fShader` to shaders diff --git a/README.tr.md b/README.tr.md index 433e56e..00ff3cf 100644 --- a/README.tr.md +++ b/README.tr.md @@ -251,6 +251,11 @@ profilinde basamak olmadığını doğruluyor. (dalga ölçeği ve yumuşatması, sesle sönme, warp hızı ve ölçeği) oradaki gibi dosyadan geliyor, yani 0 dalga ölçeği dalgayı düzleştiriyor (korpusta 102 preset). Motordan geçirilerek çizildi: her durum MilkDrop'un formülüyle 2/255 içinde aynı. +- **Preset dosyaları MilkDrop'un okuduğu gibi okunuyor.** Uyum açıkken anahtarlar büyük/küçük harfe + duyarlı, iki kez yazılmış bir anahtar MilkDrop'un bulduğu değeri (çoğunlukla ilkini) alıyor, + numaralı kod ilk eksik numarada bitiyor, tam sayı ayarları kesirlerini atıyor ve denklem satırları + MilkDrop'un yapıştırdığı gibi, `\\` yorumları dahil, yapışıyor. Korpusta 32 preset farklı + okunuyor; uyum kapalıyken eski ayrıştırıcı duruyor. - **Birleştirme shader'ları MilkDrop'un verdiği ton rengini alıyor.** MilkDrop her birleştirme shader'ına `hue_shader` olarak dört köşede yavaşça gezinen renkler veriyor, presetin `fShader`ı ne derse desin; `fShader` onları yalnız sabit yolda ölçekliyor. Motor `fShader`ı shader'lara da diff --git a/ROADMAP.md b/ROADMAP.md index 0863336..fd7b1d2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1963,13 +1963,14 @@ rest after. No version number yet. gives a part. ◀ ▶ step through a history of recipes (part → preset); a recipe whose preset was deleted is skipped. The id comes from the recipe (`md_mix`, the rule version and two 32-bit hashes; the - version is 2 since #580), so the same mash-up saved twice leaves one + version is 3 since #580), so the same mash-up saved twice leaves one file, and the version in it keeps later rule changes from overwriting earlier saves. - - *Part tests* decide which presets can give a part without parsing - them (13.5 µs a test): a non-comment equation line, an enabled wave - or shape block, a non-empty shader line (since #580, one whose stage - version MilkDrop reads as above 0). + - *Part tests* decide which presets can give a part: a non-comment + equation line, an enabled wave or shape block, a non-empty shader + line. They first worked on the text without parsing it (13.5 µs a + test); since #580 they read the file the way MilkDrop does (0.04 ms) + and a shader counts only if MilkDrop draws it. - **Mash-ups measured** on the whole 10,332-preset corpus. The part tests agree with the parser for every preset and part. A mash-up whose six parts come from one preset is that preset again for all 10,332 — @@ -2032,8 +2033,8 @@ rest after. No version number yet. the old ring nouns, the " · " in mash-up names or removing the module's script tag each fails a test. - **Fidelity follow-ups (#580)** · the fixed composite, the stage rule, - MilkDrop's defaults, the hue colour and the values that stay in the file - are done; the other audits and the file-reading differences are next. + MilkDrop's defaults, the hue colour, the values that stay in the file and + MilkDrop's way of reading a file are done; the other audits are next. - **Checked against the source.** Nullsoft's own code (jecassis/foo_vis_milk2 5b44cea) and, for the fixed pipeline's blend passes, the D3D9 code the D3D11 fork was ported from (BeatDrop @@ -2174,19 +2175,62 @@ rest after. No version number yet. flat; of the 4 with warp speed 0, one changes, slightly. With fidelity off all of them are identical. The 900-preset sample sorts into the same classes, preset for preset. + - **Files read the way MilkDrop reads them.** With fidelity on, a preset + file goes through a reader that follows MilkDrop's own + (`parseMilkMd2`; state.cpp _GetLineByName, the GetFast readers, + ReadCode and the three Import functions, the same in the D3D9 code); + with it off the old parser stays. MilkDrop first indexes the file line + by line — a line's name runs to the first `=`, space or line break — + and then looks the keys up in its own order, trying the line after the + previous read before scanning from the top. So keys are case-sensitive; + an indented line has an empty name and is never read; `key value` is + read and `key = value` is not; a key written twice takes the next line + or the first occurrence, not the last; integer keys are `%d` + (`textured=0.05` is off, where we had it on) and float keys take the + leading number (`.975;` is 0.975); numbered code ends at the first + missing number; and equation lines lose `//` and `\\` comments and are + glued with nothing between them, trailing spaces kept. A simulation of + that reader against our parser found real differences in 25 files of + the corpus; the engine-side check (`readingsDiffer`: stage versions, + the numeric keys the engine reads, equations without whitespace) finds + 32 — duplicated keys (4 presets), fractions in integer keys (11), key + case (2), a per-frame name written in the header, shapes past MilkDrop + 2's four, a `\\` comment, a gap in the numbering. Our builtins, + generated presets and mash-ups read the same both ways. Turning + fidelity over rebuilds the running preset only when the two readings + differ for it, and the stage choice reads the file the same way. + - **Mash-ups read their donors the same way.** A preset gives a part only + if MilkDrop reads that part from it, and the version lines written for a + shader are the ones MilkDrop reads from its donor — a donor writing + `PSVERSION_comp=3` gives version 2. The same recipe can give different + version lines than before, so the mash-up rule version went to 3. The + part test now reads the file (0.04 ms a test, was 0.014); on the whole + corpus it agrees with the engine for every preset and part, a mash-up of + one preset reads as that preset for all 10,332, and 5,000 random + mash-ups keep every part and stage of their donors. + - **Measured** against `main`, the last ten of 60 frames: 120 control + presets are identical with fidelity on and off; of the 32 that read + differently, 15 change with fidelity on, 12 by more than 1% (up to + 32%), and all 32 are identical with it off. The 900-preset sample sorts + into the same classes, preset for preset. - **Not done yet:** the fixed warp path, the blur chain, borders and - centre darkening, and the rest of the blend snap points; the - file-reading differences found while building the mash-ups (a - duplicated key, where MilkDrop reads the first occurrence and we keep - the last; numbered code that MilkDrop stops reading at the first - missing number; `\\` comments; integer keys holding fractions; text - after a number; key case; indented lines — about 40 corpus presets in - all); whether `uv` in preset shaders runs the way MilkDrop's does — - read back from the screen, our `uv.y` is 1 at the top, where MilkDrop's - texture coordinate is 0; sampling agrees, but a shader doing arithmetic - on `uv.y` may come out mirrored, which needs its own check; the - reference comparison with an external renderer, which needs one - installed and waits for the user's approval. + centre darkening, and the rest of the blend snap points; whether `uv` + in preset shaders runs the way MilkDrop's does — read back from the + screen, our `uv.y` is 1 at the top, where MilkDrop's texture + coordinate is 0; sampling agrees, but a shader doing arithmetic on + `uv.y` may come out mirrored, which needs its own check; MilkDrop's + internal comparison functions such as `_aboeq`, which three corpus + presets call and our compiler does not know; the reference comparison + with an external renderer, which needs one installed and waits for the + user's approval. Left out of the reader on purpose: MilkDrop reads + bytes and the engine gets decoded text, so its two byte rules — a + 0xFF byte ends the file, a value over 251 characters splits into a + second index line — cannot be kept exactly, and neither touches a + corpus file; values stay double where MilkDrop stores float, a + difference below 1e-7; and a block that fails to compile is dropped + whole in MilkDrop but recovered statement by statement here, because + our parser and MilkDrop's do not agree on what an error is (`_aboeq` + is one case), so dropping whole blocks would drop some MilkDrop runs. - **Tests.** 14 new: the version rule and the stage choice, the two generated shaders (float rounding, samplers, echo, hue, flag order, and that they translate), echo orientation and gamma cases, the @@ -2216,7 +2260,16 @@ rest after. No version number yet. counting as 1 with it off; and the volume fade's switch and range from the file with MilkDrop's defaults, a negative switch counting as on. Five test files follow the new reads, one of them with a flat custom - wave at scale 0. 16 of 16 mutations are caught. + wave at scale 0. 16 of 16 mutations are caught. The file reading adds + ten: the reader's naming (case, indentation, the space separator), the + next-line and first-occurrence lookup, `%d` and `%f`, code ending at a + gap with comments cut and lines glued, the version lines in MilkDrop's + order, `readMilk` and the Preset's `readAcc`, `readingsDiffer`, every + Preset built in `src` naming its reading rule, the toggle rebuilding + only when the readings differ, and a mash-up's version lines from its + donor's reading. The own-presets test also requires both readings to + agree, and the mash-up part test compares with the new reader. + 23 of 23 mutations are caught. ## v3.1.6 — Comprehensive video export