Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
745339a
Mount a microSD card at /sd, addressing ewowi's PR #134 review
Oct 11, 2026
b9c3f6b
Skip a no-op SD remount, and refuse one under an open stream
Oct 11, 2026
73bc49a
Move the SD card's one-shot FIL off the stack, onto one static
Oct 11, 2026
dc236f1
Make SdCardModule and its transfer slots cost nothing without SD
Oct 11, 2026
e863bd7
Drop every SD symbol from a binary that cannot use the card
Oct 11, 2026
f251d9a
Gate /sd on a mounted card, not the chip's bare capability
Oct 11, 2026
22497bf
Detect a pulled or reinserted SD card without a pin touch or reboot
Oct 11, 2026
7f1bb8d
Refuse to delete a mount's own root through the File Manager
Oct 11, 2026
b9a34f6
Build fsWriteStream on the resumable primitives, drop fsReadAt
Oct 12, 2026
0f1e9e9
Close an active SD transfer's stream and socket on release()
Oct 12, 2026
66569ad
Claim an SD download's slot before promising its body
Oct 12, 2026
d732957
Widen fsSize past the 2 GiB a 32-bit long can hold signed
Oct 12, 2026
137dc3a
Correct a stale comment on the card's write-stream close
Oct 12, 2026
577d09d
Consolidate the five /sd prefix checks into one platform::isSdPath
Oct 12, 2026
a3e092f
Document that sdCardUsed's f_getfree runs on the render thread
Oct 12, 2026
ac6ab2b
Drain several SD transfer chunks per tick, not one
Oct 12, 2026
ab9024f
Name the SD LDO channel's reason, not just its number
Oct 12, 2026
ef12547
Collapse multi-line comments and speak in the positive on the SD card
Oct 12, 2026
f27c31a
Make the SD card a service with board presets, SPI, and background fo…
ewowi Oct 12, 2026
6191272
Process CodeRabbit's review and keep SD to images over 4 MB
ewowi Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ add_library(mm_core STATIC
src/core/system/HttpServerModule.cpp
src/core/system/FilesystemModule.cpp
src/core/system/FileManagerModule.cpp
src/core/services/SdCardService.cpp
src/core/system/MqttModule.cpp
src/core/util/sha256.cpp
src/core/module/Scheduler.cpp
Expand Down
Binary file added docs/assets/core/SdCardService-files.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/core/SdCardService.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions docs/moonmodules/core/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@

Detail: [technical](moxygen/MoonLiveService.md)

<a id="sd-card"></a>

### SD Card

A board's microSD slot, where one is wired: its wiring, a usage gauge and a format button. Mounted, its files appear under `/sd`, within the one [File Manager](system.md#file-manager) tree. Behavior: ⌄ details.

<img src="../../assets/core/SdCardService.png" width="300" alt="SD Card module controls">
<img src="../../assets/core/SdCardService-files.png" width="300" alt="The card's files in the File Manager, under sd">

- `board`: the slot's wiring, from the presets this chip can wire; `Custom` sets bus and pins.
- `bus`: under `Custom`, the SD bus or the SD spec's SPI mode.
- `clkPin` … `pwrPin`: the pins under `Custom`; −1 unused, `d1`-`d3` unset is 1-bit.
- `format`: a button, press twice; erases and reformats the card as FAT32 in place.
- `card (KiB)`: read-only usage bar, used / total KiB, so a card past 4 GiB still reports correctly.

Detail: [technical](moxygen/SdCardService.md)

## Analog, details
The travel maps into the control's own range: a Select with five options takes 0..4, a bool takes on and off. A row pointed at a pad does nothing, since there is no sensible reading of a pedal held at 40% of a preset. Live `raw` and `value` readouts let a pedal be calibrated by watching it move, and reversed bounds mean inverted rather than being an error, since calibrating by moving to each end sets whichever was reached first. Without a `deadband` a resting pedal rewrites its target fifty times a second forever. Scripts reach the same hardware with `adcRead(pin)` and `adcMax()`, which is the path for a sensor whose mapping is a condition rather than a range.

Expand Down Expand Up @@ -324,3 +341,11 @@

The status line reports setup state ("set pin to receive" / "ready"), the learn prompt, a binding ("learned 0x..."), what a press did or why it did not, and an unbound code ("received 0x...
(unassigned)").

## SD Card, details

The presets: `P4 SDMMC` is the slot every P4 board with one wires, and a P4 opens on it. `Classic SDMMC (1-bit)` is the classic's own SD host pins, which the Olimex ESP32-Gateway wires, and `Classic SPI` the QuinLED Dig-Octa's. A 4 MB image carries no SD at all, so the 4 MB Olimex has none. SPI mode maps `clkPin` to SCK, `cmdPin` to MOSI, `d0Pin` to MISO and `d3Pin` to CS. Over SPI, reads hold but a write over about 50 KB can stop the card until a power cycle, so a card mounted that way says so on its status.

Mounts live, in `prepare()` rather than `setup()`: editing a pin unmounts and remounts on the new wiring, no reboot needed, the same contract the [I2C bus](system.md#i2c-bus)'s `sda`/`scl` have. A pulled card is noticed within a second. A missing or unreadable card shows its own status instead of retrying, since a mount attempt on an empty slot stalls the render thread: insert the card, then switch the module off and on, edit a pin, or reboot.

Check warning on line 349 in docs/moonmodules/core/services.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma. Raw Output: {"message":"[MoonLight.SentenceLength] Sentence runs past 30 words; it is probably two thoughts joined by a comma.","location":{"path":"docs/moonmodules/core/services.md","range":{"start":{"line":349,"column":238},"end":{"line":349,"column":239}}},"severity":"INFO","code":{"value":"MoonLight.SentenceLength"}}

The card is FAT32 only (ESP-IDF's FatFs ships with exFAT compiled out), with the SD Association's 32 KB clusters. A card without a FAT filesystem, fresh or exFAT, says so, and `format` makes one in place rather than requiring a second device. A format takes seconds on a large card, so it runs in the background: the status reads "formatting" and the card refuses transfers until it finishes. Once mounted, its content is reached through the [File Manager](system.md#file-manager) exactly like the internal flash, under an `sd` folder; this module never duplicates that tree.
2 changes: 1 addition & 1 deletion docs/reference/hardware/esp32-p4-eth.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The board uses the `P4 RMII` preset, shared with the Waveshare P4-NANO: MDC 31,

## microSD slot

Wired on the board; MoonLight does not mount it:
Wired on the board and mounted by [SdCardService](../../moonmodules/core/services.md#sd-card):

| Signal | GPIO |
|---|---|
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/hardware/gpio-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ For **LED output** specifically — the pins a WS2812-class strand data line can
| Role-conflict | **34-38** | Strapping pins (boot mode). Don't drive at reset; the first LED-driver default wrongly landed here (see [lessons.md](../../work/past/lessons.md)). |
| Role-conflict | **37, 38** | UART0 console on the P4-NANO (`CONFIG_ESP_CONSOLE_UART_DEFAULT`) — the runtime `ESP_LOGI` lines come out here, not over USB. |
| Board-wired (P4-NANO) | **Ethernet RMII** 28-31 / 49-52, **C6 SDIO** 14-19 / 54, **I2C** 7-8 | Consumed by the NANO's on-board Ethernet PHY, the C6 WiFi co-processor, and the I2C bus. |
| Board-wired (P4-ETH) | **Ethernet RMII** 28-31 / 34-35 / 49-52, **ES8311 audio** 7-13, **USB D-/D+** 24-25, **microSD** (unimplemented) 39-45 | The [Waveshare ESP32-P4-ETH](esp32-p4-eth.md) has no C6/WiFi. It carries its own onboard ES8311 codec, a native USB-C port, and a microSD slot; that page has the full per-signal map. |
| Board-wired (P4-ETH) | **Ethernet RMII** 28-31 / 34-35 / 49-52, **ES8311 audio** 7-13, **USB D-/D+** 24-25, **microSD** 39-45 | The [Waveshare ESP32-P4-ETH](esp32-p4-eth.md) has no C6/WiFi. It carries its own onboard ES8311 codec, a native USB-C port, and a microSD slot. That page has the full per-signal map. |

**Clear on the P4-NANO:** 20-27, 32-33, 39-48 (the LED-driver default is `pins="20,21,22,23,24,25,26,27"`). Exact free set is board-specific; the NANO's is the reference. A carrier board changes what's exposed and how. The [MHC-WLED ESP32-P4 shield](mhc-wled-esp32-p4-shield.md) routes every terminal through level shifters, RS-485 transceivers, or protected inputs, with no bare GPIO. A direct loopback jumper fails on it for that reason. Its full terminal map is on that page.

Expand Down
42 changes: 23 additions & 19 deletions docs/reference/metrics/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ Generated by [`moondeck/check/check_code.py`](../../../moondeck/check/check_code

Every function, block and file over a limit the [coding standards](../../contributing/coding-standards.md#static-checks) set. Current state only: the trend is this file's git history. The list only shrinks, per rule and in total, and a touched file clears its own rows.

**1620 finding(s)** across 318 file(s). 2.88% of all lines are duplicated, 3733 lines.
**1629 finding(s)** across 322 file(s). 2.83% of all lines are duplicated, 3732 lines.

## By rule

| Rule | Findings |
|---|---:|
| duplicated block | 768 |
| complex function | 283 |
| blocking call on the render path | 189 |
| complex function | 290 |
| blocking call on the render path | 192 |
| deeply nested | 113 |
| module type in the UI | 71 |
| long function | 49 |
| large stack frame | 38 |
| large stack frame | 37 |
| long parameter list | 34 |
| raw allocation in light | 28 |
| large file | 25 |
Expand Down Expand Up @@ -44,13 +44,13 @@ Each rule is a code smell with a named move from the refactoring catalog, the ta

| Area | Findings |
|---|---:|
| `test` | 570 |
| `test` | 574 |
| `light` | 421 |
| `core` | 351 |
| `platform` | 173 |
| `core` | 357 |
| `platform` | 170 |
| `ui` | 74 |
| `moondeck` | 24 |
| `src` | 5 |
| `src` | 7 |
| `mooninstaller` | 2 |

## By file
Expand All @@ -59,11 +59,11 @@ The next function worth simplifying is at the top of each file's list.

| File | Findings | Worst |
|---|---:|---|
| `src/core/system/HttpServerModule.cpp` | 44 | `HttpServerModule::handleConnection`, complex function 121 |
| `src/core/system/HttpServerModule.cpp` | 48 | `HttpServerModule::handleConnection`, complex function 123 |
| `src/ui/migrate.js` | 43 | `line 29: 'I2cBusModule'`, module type in the UI 1 |
| `src/platform/desktop/platform_desktop.cpp` | 41 | `platform::spawnEncoderProcess`, complex function 58 |
| `src/core/system/MqttModule.cpp` | 31 | `MqttModule::routePublish`, complex function 29 |
| `src/ui/app.js` | 29 | `app.js`, large file 9033 |
| `src/ui/app.js` | 29 | `app.js`, large file 9076 |
| `src/core/moonlive/MoonLiveCompiler.cpp` | 27 | `moonlive::Parser::parseCall`, complex function 59 |
| `src/light/drivers/ParallelLedDriver.h` | 27 | `ParallelLedDriver::reinit`, complex function 36 |
| `src/light/drivers/PreviewDriver.h` | 27 | `PreviewDriver::buildCoordTable`, complex function 37 |
Expand All @@ -80,7 +80,7 @@ The next function worth simplifying is at the top of each file's list.
| `test/unit/core/unit_HttpServerModule_apply.cpp` | 19 | `lines 536-547, also in test/unit/core/unit_HttpServerModule_apply.cpp:122`, duplicated block 12 |
| `src/platform/esp32/platform_esp32_moon_i80.cpp` | 18 | `platform::createRingState`, complex function 32 |
| `test/unit/core/unit_PinsModule.cpp` | 18 | `lines 46-57, also in test/unit/light/unit_Hub75Driver_pins.cpp:66`, duplicated block 12 |
| `src/core/services/AudioService.h` | 17 | `AudioService in 49 files`, name spread 49 |
| `src/core/services/AudioService.h` | 17 | `AudioService in 51 files`, name spread 51 |
| `src/light/effects/PacmanEffect.h` | 16 | `lines 243-257, also in src/light/effects/FishTankEffect.h:244`, duplicated block 15 |
| `src/platform/esp32/platform_esp32_ota.cpp` | 15 | `platform::otaWriteMoonBase`, complex function 33 |
| `test/unit/light/unit_PaintBrushEffect.cpp` | 15 | `lines 44-61, also in test/unit/light/unit_GEQ3DEffect.cpp:58`, duplicated block 18 |
Expand Down Expand Up @@ -133,7 +133,6 @@ The next function worth simplifying is at the top of each file's list.
| `src/light/drivers/NetworkSendDriver.h` | 6 | `NetworkSendDriver::tick, line 190`, large stack frame 1760 |
| `src/light/layers/BlendMap.h` | 6 | `blendMap`, complex function 35 |
| `src/platform/desktop/moonlive_asm_x86_64.cpp` | 6 | `lines 557-564, also in src/platform/desktop/moonlive_asm_x86_64.cpp:515`, duplicated block 8 |
| `src/platform/esp32/platform_esp32_fs.cpp` | 6 | `lines 211-221, also in src/platform/esp32/platform_esp32_fs.cpp:161`, duplicated block 11 |
| `src/platform/esp32/platform_esp32_improv.cpp` | 6 | `platform::improvTask`, complex function 22 |
| `test/unit/core/unit_AccessPointModule.cpp` | 6 | `lines 340-348, also in test/unit/core/unit_AccessPointModule.cpp:311`, duplicated block 9 |
| `test/unit/core/unit_ImprovFrame.cpp` | 6 | `lines 179-187, also in test/unit/core/unit_ImprovFrame.cpp:152`, duplicated block 9 |
Expand Down Expand Up @@ -168,7 +167,7 @@ The next function worth simplifying is at the top of each file's list.
| `src/core/moonlive/MoonLiveScriptFile.h` | 4 | `moonlive::compileScriptFile`, complex function 20 |
| `src/core/services/ButtonService.h` | 4 | `lines 96-118, also in src/core/services/InfraredService.h:113`, duplicated block 23 |
| `src/core/system/EthernetModule.h` | 4 | `EthernetModule::defineControls`, complex function 24 |
| `src/core/system/FilesystemModule.cpp` | 4 | `FilesystemModule::moduleIndexForConfigPath`, complex function 13 |
| `src/core/system/FilesystemModule.cpp` | 4 | `FilesystemModule::removeTree`, complex function 13 |
| `src/core/system/ImprovProvisioningModule.h` | 4 | `NetworkModule::setTxPowerSetting in tick1s`, blocking call on the render path 1 |
| `src/core/system/TasksModule.h` | 4 | `TasksModule::TaskListSource::writeListRowDetail`, complex function 11 |
| `src/light/drivers/ParallelSlots.h` | 4 | `encodeWs2812ShiftData`, complex function 19 |
Expand Down Expand Up @@ -208,7 +207,9 @@ The next function worth simplifying is at the top of each file's list.
| `src/light/effects/PongEffect.h` | 3 | `PongEffect::render`, complex function 15 |
| `src/light/effects/SolidEffect.h` | 3 | `SolidEffect::static_cast<lengthType>`, complex function 36 |
| `src/light/layouts/GridLayout.h` | 3 | `GridLayout in 86 files`, name spread 86 |
| `src/module_types.cpp` | 3 | `registerModuleTypes`, long function 150 |
| `src/platform/desktop/moonlive_asm_arm64.cpp` | 3 | `lines 35-40, also in src/platform/esp32/moonlive_asm_riscv.cpp:34`, duplicated block 6 |
| `test/unit/core/unit_HttpServerModule_sdcard.cpp` | 3 | `lines 24-33, also in test/unit/core/unit_HttpServerModule_connections.cpp:18`, duplicated block 10 |
| `test/unit/core/unit_NetworkModule_ethernet.cpp` | 3 | `lines 276-285, also in test/unit/core/unit_NetworkModule_ethernet.cpp:193`, duplicated block 10 |
| `test/unit/light/unit_AuroraEffect.cpp` | 3 | `lines 40-48, also in test/unit/light/unit_PolarLut_equivalence.cpp:40`, duplicated block 9 |
| `test/unit/light/unit_MoonLiveLayout.cpp` | 3 | `lines 685-935, also in test/unit/light/unit_MoonLiveLayout.cpp:403`, duplicated block 251 |
Expand All @@ -227,6 +228,7 @@ The next function worth simplifying is at the top of each file's list.
| `moondeck/scenario/run_scenario.py` | 2 | `lines 33-39, also in moondeck/docs/build_docs.py:43`, duplicated block 7 |
| `src/core/moonlive/MoonLiveBuiltins.h` | 2 | `lines 208-219, also in src/core/moonlive/MoonLiveBuiltins.h:111`, duplicated block 12 |
| `src/core/services/MidiService.h` | 2 | `MidiService::serveShare in tick20ms`, blocking call on the render path 1 |
| `src/core/services/SdCardService.cpp` | 2 | `SdCardService::finishFormat in tick1s`, blocking call on the render path 1 |
| `src/core/system/DevicePlugin.h` | 2 | `lines 94-100, also in src/core/system/DevicePlugin.h:75`, duplicated block 7 |
| `src/core/system/HttpServerModule.h` | 2 | `HttpServerModule in 17 files`, name spread 17 |
| `src/core/system/I2cBusModule.h` | 2 | `I2cBusModule::scan`, complex function 18 |
Expand Down Expand Up @@ -259,6 +261,7 @@ The next function worth simplifying is at the top of each file's list.
| `src/platform/esp32/moonlive_asm_riscv.h` | 2 | `lines 74-91, also in src/platform/esp32/moonlive_asm_xtensa.h:79`, duplicated block 18 |
| `src/platform/esp32/moonlive_asm_xtensa.cpp` | 2 | `lines 481-487, also in src/platform/esp32/moonlive_asm_riscv.cpp:297`, duplicated block 7 |
| `src/platform/esp32/moonlive_emit_riscv.cpp` | 2 | `lines 52-57, also in src/platform/esp32/moonlive_emit_riscv.cpp:25`, duplicated block 6 |
| `src/platform/esp32/platform_esp32_fs.cpp` | 2 | `platform::fsList`, complex function 17 |
| `test/js/ui-midi-bridge.test.mjs` | 2 | `lines 4-16, also in test/js/ui-gamepad-bridge.test.mjs:4`, duplicated block 13 |
| `test/unit/core/unit_AudioService_sync.cpp` | 2 | `lines 313-322, also in test/unit/core/unit_AudioService_sync.cpp:269`, duplicated block 10 |
| `test/unit/core/unit_DevicesModule_hue.cpp` | 2 | `lines 63-69, also in test/unit/core/unit_DevicesModule_hue.cpp:42`, duplicated block 7 |
Expand Down Expand Up @@ -301,9 +304,9 @@ The next function worth simplifying is at the top of each file's list.
| `src/core/services/AudioLevel.h` | 1 | `computeLevel`, complex function 14 |
| `src/core/services/MoonLiveService.h` | 1 | `moonlive::MoonLive::runValue in tick20ms`, blocking call on the render path 1 |
| `src/core/services/OscPacket.h` | 1 | `osc::parse`, complex function 32 |
| `src/core/services/Services.h` | 1 | `Services in 12 files`, name spread 12 |
| `src/core/services/Services.h` | 1 | `Services in 13 files`, name spread 13 |
| `src/core/system/FileManagerModule.cpp` | 1 | `platform::filesystemUsed in tick1s`, blocking call on the render path 1 |
| `src/core/system/FilesystemModule.h` | 1 | `FilesystemModule in 24 files`, name spread 24 |
| `src/core/system/FilesystemModule.h` | 1 | `FilesystemModule in 25 files`, name spread 25 |
| `src/core/util/Addressing.h` | 1 | `sendAddressed`, complex function 11 |
| `src/core/util/CaptivePortal.h` | 1 | `captive::dnsReply`, complex function 21 |
| `src/core/util/ImprovFrame.h` | 1 | `ImprovFrameParser::feed`, complex function 22 |
Expand Down Expand Up @@ -339,14 +342,14 @@ The next function worth simplifying is at the top of each file's list.
| `src/light/moonlive/MoonLivePalette.h` | 1 | `moonlive::MoonLive::runValue in tick`, blocking call on the render path 1 |
| `src/light/powerfunctions/fluid.h` | 1 | `Fluid::resize`, complex function 14 |
| `src/light/util/E131Packet.h` | 1 | `parseE131Packet`, complex function 17 |
| `src/module_types.cpp` | 1 | `registerModuleTypes`, long function 149 |
| `src/platform/desktop/moonlive_asm_host.h` | 1 | `lines 36-43, also in src/platform/esp32/moonlive_asm_riscv.h:36`, duplicated block 8 |
| `src/platform/desktop/platform_config.h` | 1 | `lines 71-85, also in src/platform/esp32/platform_config.h:236`, duplicated block 15 |
| `src/platform/desktop/platform_config.h` | 1 | `lines 71-85, also in src/platform/esp32/platform_config.h:240`, duplicated block 15 |
| `src/platform/esp32/moonlive_asm_xtensa.h` | 1 | `lines 79-96, also in src/platform/esp32/moonlive_asm_riscv.h:74`, duplicated block 18 |
| `src/platform/esp32/platform_config.h` | 1 | `lines 236-250, also in src/platform/desktop/platform_config.h:71`, duplicated block 15 |
| `src/platform/esp32/platform_config.h` | 1 | `lines 240-254, also in src/platform/desktop/platform_config.h:71`, duplicated block 15 |
| `src/platform/esp32/platform_esp32_i2s.cpp` | 1 | `platform::audioMicInit`, complex function 11 |
| `src/platform/esp32/platform_esp32_sdmmc.cpp` | 1 | `platform::mountCard`, complex function 12 |
| `src/platform/esp32/platform_esp32_tasks.cpp` | 1 | `platform::taskSnapshot`, complex function 12 |
| `src/platform/platform.h` | 1 | `platform.h`, large file 1301 |
| `src/platform/platform.h` | 1 | `platform.h`, large file 1401 |
| `src/ui/install-picker.js` | 1 | `install-picker.js`, large file 1048 |
| `src/ui/preview3d.js` | 1 | `preview3d.js`, large file 1431 |
| `test/js/installer-eth-only.test.mjs` | 1 | `lines 13-23, also in test/js/ui-mooncloud.test.mjs:6`, duplicated block 11 |
Expand All @@ -362,6 +365,7 @@ The next function worth simplifying is at the top of each file's list.
| `test/scenario_runner.cpp` | 1 | `scenario_runner.cpp`, large file 1075 |
| `test/unit/core/unit_ControlModule.cpp` | 1 | `unit_ControlModule.cpp`, large file 2152 |
| `test/unit/core/unit_Control_apply_absent_key.cpp` | 1 | `lines 92-103, also in test/unit/core/unit_HttpServerModule_apply.cpp:40`, duplicated block 12 |
| `test/unit/core/unit_HttpServerModule_connections.cpp` | 1 | `lines 18-27, also in test/unit/core/unit_HttpServerModule_sdcard.cpp:24`, duplicated block 10 |
| `test/unit/core/unit_moonlive_compiler.cpp` | 1 | `unit_moonlive_compiler.cpp`, large file 1865 |
| `test/unit/core/unit_moonlive_fill.cpp` | 1 | `unit_moonlive_fill.cpp`, large file 1380 |
| `test/unit/light/unit_BreakoutEffect.cpp` | 1 | `lines 18-27, also in test/unit/light/unit_TetrixEffect.cpp:20`, duplicated block 10 |
Expand Down
Loading
Loading