Character creator: hair grown from the body's own scalp #120

Merged
kompas-review merged 1 commit from khyretos/claude/creator-hair into main 2026-10-25 16:31:45 +01:00 AGit
Owner

Builds on PR 116 (claude/character-creator), which builds on PR 115: merge those first; this one then shows only its own commit.

What changes

  • kke/HumanHair.h: the engine's strand hair on any MakeHuman head. findScalp finds where hair grows on the shaped body (the head bone's skin above the brows and ear tops, behind the hairline; measured from the eyes, so it scales with the head) and fits a sphere. hairOnHuman lays any hairstyle on it and sets each root on the skin straight under it (ray against the scalp's faces); loose styles start back from the hairline so they don't fall over the eyes.
  • Character creator: a Hair tab with 14 hairstyles (short, straight, long, wavy, curly, afro, puff, high-top fade, twist-out, bantu knots, box braids, cornrows, locs, twists), 11 natural colours and a length slider. The body is a mesh collider only hair and cloth feel; Turn spins the figure so the hair swings; the scalp is painted the root colour; hair regrows on the new head once a slider rests. Random person picks a hairstyle; grey/white mostly for older people.
  • CI self-test also checks the random hairstyle is on the head. Env for screenshots: KKE_CREATOR_HAIR, KKE_CREATOR_TURN.

How I verified it

  • ./build/bin/kke_tests: 1260 passed (3 new: scalp found for both genders and never on the face; roots on the skin for short, long, afro, cornrows on a taller head)
  • Build with warnings as errors
  • character_creator under lavapipe with Vulkan validation + sync validation, turning, long hair: no errors; self-test passed three runs with random styles
  • Screenshots of short, long, wavy, afro, box braids, random people (front, profile, back)

Performance

160 guide strands in a one-worker RigidWorld; drawn hairs built on the GPU (HairRenderer). Only while the creator is open.

Docs

  • docs/HUMANS.md "Hair", docs/HAIR.md pointer, games/character_creator/README.md (screenshot, controls, env), ROADMAP

UI change: holds for Kees's look-OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6

Builds on PR 116 (claude/character-creator), which builds on PR 115: merge those first; this one then shows only its own commit. ## What changes - `kke/HumanHair.h`: the engine's strand hair on any MakeHuman head. `findScalp` finds where hair grows on the shaped body (the head bone's skin above the brows and ear tops, behind the hairline; measured from the eyes, so it scales with the head) and fits a sphere. `hairOnHuman` lays any hairstyle on it and sets each root on the skin straight under it (ray against the scalp's faces); loose styles start back from the hairline so they don't fall over the eyes. - Character creator: a Hair tab with 14 hairstyles (short, straight, long, wavy, curly, afro, puff, high-top fade, twist-out, bantu knots, box braids, cornrows, locs, twists), 11 natural colours and a length slider. The body is a mesh collider only hair and cloth feel; Turn spins the figure so the hair swings; the scalp is painted the root colour; hair regrows on the new head once a slider rests. Random person picks a hairstyle; grey/white mostly for older people. - CI self-test also checks the random hairstyle is on the head. Env for screenshots: `KKE_CREATOR_HAIR`, `KKE_CREATOR_TURN`. ## How I verified it - [x] `./build/bin/kke_tests`: 1260 passed (3 new: scalp found for both genders and never on the face; roots on the skin for short, long, afro, cornrows on a taller head) - [x] Build with warnings as errors - [x] `character_creator` under lavapipe with Vulkan validation + sync validation, turning, long hair: no errors; self-test passed three runs with random styles - [x] Screenshots of short, long, wavy, afro, box braids, random people (front, profile, back) ## Performance 160 guide strands in a one-worker RigidWorld; drawn hairs built on the GPU (HairRenderer). Only while the creator is open. ## Docs - [x] docs/HUMANS.md "Hair", docs/HAIR.md pointer, games/character_creator/README.md (screenshot, controls, env), ROADMAP UI change: holds for Kees's look-OK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6
tools/makehuman/cook_human.py cooks MakeHuman's CC0 data (base mesh,
1,280 morph targets, 291 modifiers, default rig and weights) into
assets/humans/makehuman.kkhpack: exact to the .target files' 0.1 mm
steps, sparse and zlib-packed, 127 MB of text to 9.8 MB. Only the data
files are read; none of MakeHuman's AGPL code is used.

kke::HumanPack reads it; kke::Human sets sliders by MakeHuman's own
names, blends the macro targets (gender, age, muscle, weight, height,
proportions, ancestry, breast), and bakes a skinned ModelData whose
skeleton is fitted to the shaped body; reshape() moves a bake in place
while a slider is dragged.

Tests: 8 in tests/test_human.cpp (incl. the rest pose reproducing the
shape exactly and reshape matching a fresh bake), 7 for the cooker,
run in CI. docs/HUMANS.md, ROADMAP, DEPENDENCIES updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6
Human test: one copy of the target weights, not two
All checks were successful
review / pr-agent PR-Agent reviewed this commit
CI / dependencies (pull_request) Successful in 5s
CI / std-includes (pull_request) Successful in 11s
Benchmarks / benchmarks (pull_request) Successful in 2m50s
CI / shipping-build (pull_request) Successful in 13m43s
Docs site / site (pull_request) Successful in 32s
lint / megalinter (pull_request) Successful in 3m2s
Windows (native) / build (pull_request) Successful in 23m35s
CI / build-test-and-coverage (pull_request) Successful in 38m52s
CI / android (pull_request) Successful in 33m50s
Windows (native) / test (pull_request) Successful in 6m21s
CI / release-build (pull_request) Successful in 46m8s
Windows (native) / publish (pull_request) Has been skipped
kk / all-checks All 13 checks passed
4e34257c6a
begin() and end() came from two temporary vectors; the full suite
crashed on it, a filtered run happened not to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6
Character creator: a screen any game can add, and its demo
All checks were successful
review / pr-agent PR-Agent reviewed this commit
CI / dependencies (pull_request) Successful in 8s
CI / std-includes (pull_request) Successful in 11s
Benchmarks / benchmarks (pull_request) Successful in 2m52s
CI / shipping-build (pull_request) Successful in 12m42s
Docs site / site (pull_request) Successful in 47s
CI / android (pull_request) Successful in 12m31s
lint / megalinter (pull_request) Successful in 3m3s
CI / build-test-and-coverage (pull_request) Successful in 39m20s
CI / release-build (pull_request) Successful in 43m44s
Windows (native) / build (pull_request) Successful in 55m44s
Windows (native) / test (pull_request) Successful in 6m27s
Windows (native) / publish (pull_request) Has been skipped
kk / all-checks All 13 checks passed
8894eb17ff
kke::CharacterCreatorModule is the screen: tabs of MakeHuman's sliders
(body type, shape, torso, arms and legs, head and neck, eyes, nose,
mouth and jaw, ears, skin), mirrored sides, undo and redo, a believable
randomise, camera views, save and load, and a share code. While a slider
moves only vertices move; bakeSkinned() gives the game a skinned body.

kke/HumanLook.h is the look that travels: slider values and colours as
JSON saves (and MakeHuman's .mhm), and as a share code of one bit per
slider plus a byte per moved one (about 90 to 250 letters), tied to the
pack's slider list. quantised() rounds the player's own body like the
code, so every machine bakes the same vertices.

Human now stands on the floor (feet at y = 0), keeps the macro shape
between changes, and gains setAll for whole looks.

games/character_creator is the demo. CI runs it under validation and a
self-test (a random person through a share code and back), which found
two codes for one look; fixed, with a unit test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6
Character creator: hair grown from the body's own scalp
All checks were successful
review / pr-agent PR-Agent reviewed this commit
CI / dependencies (pull_request) Successful in 5s
CI / std-includes (pull_request) Successful in 11s
CI / shipping-build (pull_request) Successful in 12m35s
Benchmarks / benchmarks (pull_request) Successful in 3m18s
CI / android (pull_request) Successful in 5m28s
Docs site / site (pull_request) Successful in 10s
lint / megalinter (pull_request) Successful in 2m57s
Windows (native) / build (pull_request) Successful in 9m44s
Windows (native) / test (pull_request) Successful in 6m26s
CI / release-build (pull_request) Successful in 46m39s
Windows (native) / publish (pull_request) Has been skipped
CI / build-test-and-coverage (pull_request) Successful in 2h4m56s
kk / all-checks All 13 checks passed
8c8a689299
kke/HumanHair.h puts the engine's strand hair on any MakeHuman head:
findScalp finds where hair grows (the head bone's skin above the brows
and ear tops, behind the hairline, measured from the eyes so it scales
with the head) and fits a sphere; hairOnHuman lays any hairstyle on it
and sets each root on the skin under it with a ray against the scalp's
faces, dropping roots with no scalp under them. Loose styles start back
from the hairline, since a real face stands out of that sphere.

The creator gets a Hair tab: 14 hairstyles (short to long, afro, puff,
fade, twist-out, bantu knots, box braids, cornrows, locs, twists), 11
natural colours and a length. The body is a mesh collider only cloth and
hair feel, in a RigidWorld of the creator's own; Turn spins the figure so
the hair swings; the scalp is painted the root colour; the hair regrows
on a new head once a slider rests. Random person picks a hairstyle too,
and the CI self-test checks it is on the head.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6
kompas-review scheduled this pull request to auto merge when all checks succeed 2026-10-25 04:41:07 +01:00
Collaborator

Title

Character creator: hair grown from the body's own scalp


User description

Builds on PR 116 (claude/character-creator), which builds on PR 115: merge those first; this one then shows only its own commit.

What changes

  • kke/HumanHair.h: the engine's strand hair on any MakeHuman head. findScalp finds where hair grows on the shaped body (the head bone's skin above the brows and ear tops, behind the hairline; measured from the eyes, so it scales with the head) and fits a sphere. hairOnHuman lays any hairstyle on it and sets each root on the skin straight under it (ray against the scalp's faces); loose styles start back from the hairline so they don't fall over the eyes.
  • Character creator: a Hair tab with 14 hairstyles (short, straight, long, wavy, curly, afro, puff, high-top fade, twist-out, bantu knots, box braids, cornrows, locs, twists), 11 natural colours and a length slider. The body is a mesh collider only hair and cloth feel; Turn spins the figure so the hair swings; the scalp is painted the root colour; hair regrows on the new head once a slider rests. Random person picks a hairstyle; grey/white mostly for older people.
  • CI self-test also checks the random hairstyle is on the head. Env for screenshots: KKE_CREATOR_HAIR, KKE_CREATOR_TURN.

How I verified it

  • ./build/bin/kke_tests: 1260 passed (3 new: scalp found for both genders and never on the face; roots on the skin for short, long, afro, cornrows on a taller head)
  • Build with warnings as errors
  • character_creator under lavapipe with Vulkan validation + sync validation, turning, long hair: no errors; self-test passed three runs with random styles
  • Screenshots of short, long, wavy, afro, box braids, random people (front, profile, back)

Performance

160 guide strands in a one-worker RigidWorld; drawn hairs built on the GPU (HairRenderer). Only while the creator is open.

Docs

  • docs/HUMANS.md "Hair", docs/HAIR.md pointer, games/character_creator/README.md (screenshot, controls, env), ROADMAP

UI change: holds for Kees's look-OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6


PR Type

Enhancement, Tests, Documentation


Description

  • Adds hair system to character creator with 14 styles

  • Implements scalp detection and root placement logic

  • Includes UI tabs, save/load, and random generation

  • Adds CI tests for hair placement on various heads


Diagram Walkthrough

flowchart LR
  A["Character Creator"] -->|"UI & Logic"| B["HumanLook"]
  A -->|"Hair Config"| C["HumanHair"]
  C -->|"findScalp"| D["Human Mesh"]
  C -->|"hairOnHuman"| E["Strand Hair"]
  D -.->|"Roots on Skin"| E
  F["Tests"] -.->|"Verify"| C
  G["Docs"] -.->|"Describe"| H["HAIR.md"]

File Walkthrough

Relevant files
Enhancement
11 files
CharacterCreatorModule.cpp
Implements character creator with hair system, UI tabs and physics
+870/-0 
Human.cpp
Adds scalp finding and hair root placement for Human class
+433/-0 
HumanPack.cpp
Parses human pack data including modifiers and targets     
+299/-0 
main.cpp
Standalone executable for the character creator demo         
+40/-0   
HumanLook.cpp
HumanLook JSON/Share code for saves and multiplayer           
+220/-0 
HumanHair.cpp
findScalp and hairOnHuman for strand hair on MakeHuman heads
+166/-0 
CharacterCreatorModule.h
CharacterCreatorModule interface with hair, save and share code
+169/-0 
HumanPack.h
HumanPack class to read the binary MakeHuman data file     
+109/-0 
Human.h
Human class to blend morph targets into a skinned model   
+93/-0   
HumanLook.h
HumanLook struct for saving looks and generating share codes
+58/-0   
HumanHair.h
HumanScalp and hairOnHuman declarations for hair on bodies
+38/-0   
Tests
5 files
test_human.cpp
Tests scalp detection and hair roots on skin for various styles
+331/-0 
ci.yml
Adds character_creator to CI self-test loop                           
+1/-1     
test_cook_human.py
Unit tests for the human pack cooker format                           
+182/-0 
ci.yml
CI job runs cook_human tests and creator self-test             
+13/-1   
CMakeLists.txt
Include test_human.cpp in the kke_tests executable             
+1/-0     
Configuration changes
5 files
new_game.cmake
Registers character_creator as a reserved game name           
+1/-1     
creator.rcss
Night theme stylesheet for the character creator UI           
+73/-0   
CMakeLists.txt
Adds kke_use_character_creator function and demo subdirectory
+21/-0   
game.json
Define game metadata and required modules for character creator
+9/-0     
CMakeLists.txt
Configure CMake build rules for the character creator game
+6/-0     
Dependencies
1 files
cook_human.py
Cooks MakeHuman CC0 data into a binary .kkhpack file         
+400/-0 
Documentation
10 files
HUMANS.md
Documentation for humans, creator screen and hair system 
+176/-0 
README.md
Demo README explaining controls and build notes                   
+85/-0   
ROADMAP.md
Adds Humans (MakeHuman) entry to the project roadmap         
+1/-0     
DEPENDENCIES.md
Lists MakeHuman assets and creator screen as dependencies
+2/-0     
index.md
Links character_creator demo in the demos index                   
+1/-0     
mkdocs.yml
Add Character Creator and Humans to documentation navigation
+2/-0     
README.md
Add character_creator demo to the list of available demos
+1/-0     
README.md
Link HUMANS.md in the documentation index                               
+1/-0     
credits.json
Add CC0 license credits for MakeHuman base mesh and data 
+16/-0   
HAIR.md
Document hairOnHuman function for MakeHuman scalp placement
+4/-0     
Additional files
6 files
makehuman.kkhpack [link]   
creator.rml +77/-0   
CMakeLists.txt +5/-0     
character-creator-face.webp [link]   
character-creator-hair.webp [link]   
character-creator.webp [link]   

## Title Character creator: hair grown from the body's own scalp ___ ### **User description** Builds on PR 116 (claude/character-creator), which builds on PR 115: merge those first; this one then shows only its own commit. ## What changes - `kke/HumanHair.h`: the engine's strand hair on any MakeHuman head. `findScalp` finds where hair grows on the shaped body (the head bone's skin above the brows and ear tops, behind the hairline; measured from the eyes, so it scales with the head) and fits a sphere. `hairOnHuman` lays any hairstyle on it and sets each root on the skin straight under it (ray against the scalp's faces); loose styles start back from the hairline so they don't fall over the eyes. - Character creator: a Hair tab with 14 hairstyles (short, straight, long, wavy, curly, afro, puff, high-top fade, twist-out, bantu knots, box braids, cornrows, locs, twists), 11 natural colours and a length slider. The body is a mesh collider only hair and cloth feel; Turn spins the figure so the hair swings; the scalp is painted the root colour; hair regrows on the new head once a slider rests. Random person picks a hairstyle; grey/white mostly for older people. - CI self-test also checks the random hairstyle is on the head. Env for screenshots: `KKE_CREATOR_HAIR`, `KKE_CREATOR_TURN`. ## How I verified it - [x] `./build/bin/kke_tests`: 1260 passed (3 new: scalp found for both genders and never on the face; roots on the skin for short, long, afro, cornrows on a taller head) - [x] Build with warnings as errors - [x] `character_creator` under lavapipe with Vulkan validation + sync validation, turning, long hair: no errors; self-test passed three runs with random styles - [x] Screenshots of short, long, wavy, afro, box braids, random people (front, profile, back) ## Performance 160 guide strands in a one-worker RigidWorld; drawn hairs built on the GPU (HairRenderer). Only while the creator is open. ## Docs - [x] docs/HUMANS.md "Hair", docs/HAIR.md pointer, games/character_creator/README.md (screenshot, controls, env), ROADMAP UI change: holds for Kees's look-OK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014ehJmtmmZLukS7FKi1aYb6 ___ ### **PR Type** Enhancement, Tests, Documentation ___ ### **Description** - Adds hair system to character creator with 14 styles - Implements scalp detection and root placement logic - Includes UI tabs, save/load, and random generation - Adds CI tests for hair placement on various heads ___ ### Diagram Walkthrough ```mermaid flowchart LR A["Character Creator"] -->|"UI & Logic"| B["HumanLook"] A -->|"Hair Config"| C["HumanHair"] C -->|"findScalp"| D["Human Mesh"] C -->|"hairOnHuman"| E["Strand Hair"] D -.->|"Roots on Skin"| E F["Tests"] -.->|"Verify"| C G["Docs"] -.->|"Describe"| H["HAIR.md"] ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>11 files</summary><table> <tr> <td><strong>CharacterCreatorModule.cpp</strong><dd><code>Implements character creator with hair system, UI tabs and physics</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/modules/CharacterCreatorModule.cpp">+870/-0</a>&nbsp; </td> </tr> <tr> <td><strong>Human.cpp</strong><dd><code>Adds scalp finding and hair root placement for Human class</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/Human.cpp">+433/-0</a>&nbsp; </td> </tr> <tr> <td><strong>HumanPack.cpp</strong><dd><code>Parses human pack data including modifiers and targets</code>&nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/HumanPack.cpp">+299/-0</a>&nbsp; </td> </tr> <tr> <td><strong>main.cpp</strong><dd><code>Standalone executable for the character creator demo</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/games/character_creator/main.cpp">+40/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>HumanLook.cpp</strong><dd><code>HumanLook JSON/Share code for saves and multiplayer</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/HumanLook.cpp">+220/-0</a>&nbsp; </td> </tr> <tr> <td><strong>HumanHair.cpp</strong><dd><code>findScalp and hairOnHuman for strand hair on MakeHuman heads</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/HumanHair.cpp">+166/-0</a>&nbsp; </td> </tr> <tr> <td><strong>CharacterCreatorModule.h</strong><dd><code>CharacterCreatorModule interface with hair, save and share code</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/include/kke/modules/CharacterCreatorModule.h">+169/-0</a>&nbsp; </td> </tr> <tr> <td><strong>HumanPack.h</strong><dd><code>HumanPack class to read the binary MakeHuman data file</code>&nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/include/kke/HumanPack.h">+109/-0</a>&nbsp; </td> </tr> <tr> <td><strong>Human.h</strong><dd><code>Human class to blend morph targets into a skinned model</code>&nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/include/kke/Human.h">+93/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>HumanLook.h</strong><dd><code>HumanLook struct for saving looks and generating share codes</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/include/kke/HumanLook.h">+58/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>HumanHair.h</strong><dd><code>HumanScalp and hairOnHuman declarations for hair on bodies</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/include/kke/HumanHair.h">+38/-0</a>&nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>5 files</summary><table> <tr> <td><strong>test_human.cpp</strong><dd><code>Tests scalp detection and hair roots on skin for various styles</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/tests/test_human.cpp">+331/-0</a>&nbsp; </td> </tr> <tr> <td><strong>ci.yml</strong><dd><code>Adds character_creator to CI self-test loop</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/.github/workflows/ci.yml">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>test_cook_human.py</strong><dd><code>Unit tests for the human pack cooker format</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/tools/makehuman/test_cook_human.py">+182/-0</a>&nbsp; </td> </tr> <tr> <td><strong>ci.yml</strong><dd><code>CI job runs cook_human tests and creator self-test</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/.forgejo/workflows/ci.yml">+13/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>CMakeLists.txt</strong><dd><code>Include test_human.cpp in the kke_tests executable</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/tests/CMakeLists.txt">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Configuration changes</strong></td><td><details><summary>5 files</summary><table> <tr> <td><strong>new_game.cmake</strong><dd><code>Registers character_creator as a reserved game name</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/tools/new_game.cmake">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>creator.rcss</strong><dd><code>Night theme stylesheet for the character creator UI</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/assets/ui/character_creator/creator.rcss">+73/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>CMakeLists.txt</strong><dd><code>Adds kke_use_character_creator function and demo subdirectory</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/CMakeLists.txt">+21/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>game.json</strong><dd><code>Define game metadata and required modules for character creator</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/games/character_creator/game.json">+9/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>CMakeLists.txt</strong><dd><code>Configure CMake build rules for the character creator game</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/games/character_creator/CMakeLists.txt">+6/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>cook_human.py</strong><dd><code>Cooks MakeHuman CC0 data into a binary .kkhpack file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/tools/makehuman/cook_human.py">+400/-0</a>&nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>10 files</summary><table> <tr> <td><strong>HUMANS.md</strong><dd><code>Documentation for humans, creator screen and hair system</code>&nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/docs/HUMANS.md">+176/-0</a>&nbsp; </td> </tr> <tr> <td><strong>README.md</strong><dd><code>Demo README explaining controls and build notes</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/games/character_creator/README.md">+85/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>ROADMAP.md</strong><dd><code>Adds Humans (MakeHuman) entry to the project roadmap</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/ROADMAP.md">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>DEPENDENCIES.md</strong><dd><code>Lists MakeHuman assets and creator screen as dependencies</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/docs/DEPENDENCIES.md">+2/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>index.md</strong><dd><code>Links character_creator demo in the demos index</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/docs/demos/index.md">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>mkdocs.yml</strong><dd><code>Add Character Creator and Humans to documentation navigation</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/mkdocs.yml">+2/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong><dd><code>Add character_creator demo to the list of available demos</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/README.md">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong><dd><code>Link HUMANS.md in the documentation index</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/docs/README.md">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>credits.json</strong><dd><code>Add CC0 license credits for MakeHuman base mesh and data</code>&nbsp; </dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/assets/humans/credits.json">+16/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>HAIR.md</strong><dd><code>Document hairOnHuman function for MakeHuman scalp placement</code></dd></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/docs/HAIR.md">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>6 files</summary><table> <tr> <td><strong>makehuman.kkhpack</strong></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/assets/humans/makehuman.kkhpack">[link]</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>creator.rml</strong></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/assets/ui/character_creator/creator.rml">+77/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>CMakeLists.txt</strong></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/CMakeLists.txt">+5/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>character-creator-face.webp</strong></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/website/static/media/character-creator-face.webp">[link]</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>character-creator-hair.webp</strong></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/website/static/media/character-creator-hair.webp">[link]</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>character-creator.webp</strong></td> <td><a href="https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/website/static/media/character-creator.webp">[link]</a>&nbsp; &nbsp; </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
Collaborator

PR Reviewer Guide 🔍

📋 Focus areas: 🔴 6 open · ✅ 0 handled. Every push re-checks them; a handled area moves to Resolved findings at the bottom.

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Missing Error Handling for File Operations

The save() function (lines 763-769) opens a file for writing without checking if the path is valid or if the file was successfully opened before attempting to write. If std::ofstream fails to open the file (e.g., due to permissions, disk full, or invalid path), the code proceeds to write JSON to a potentially invalid stream object, which will fail silently or corrupt data. The function should check if the stream is good after opening and report an error status if it fails.

void CharacterCreatorModule::save() {
    const std::string path = savePath();
    if (path.empty()) return setStatus("This device has no save folder");
    std::ofstream out(path, std::ios::binary);
    out << lookToJson(look());
    setStatus(out ? "Saved" : "Couldn't save to " + path);
}
Potential Memory Leak on Shutdown

In the shutdown() function (lines 784-792), m_hairDrawn is retired but not explicitly reset to nullptr. While the retire call likely handles cleanup, failing to nullify the pointer could lead to dangling references if the module is re-initialized or if other parts of the code expect a null pointer after shutdown. It is best practice to set m_hairDrawn = nullptr; after retiring.

void CharacterCreatorModule::shutdown() {
    if (m_hairDrawn) m_app->renderer().retire(std::move(m_hairDrawn));
    m_world.reset();
    if (m_doc) {
        m_doc->Close();
        m_doc = nullptr;
    }
    if (m_ui && m_ui->context()) m_ui->context()->RemoveDataModel("creator");
}

The load() function (lines 771-782) reads a file into a stringstream and then parses it. If the file is empty or contains invalid JSON, lookFromJson returns false and sets an error message in status, but the function does not explicitly clear any previous state or handle the case where the file exists but is unreadable (e.g., permission denied). Additionally, if path.empty() is true (no save folder), the function returns early without setting a status message, leaving the user unaware that the load operation was skipped.

void CharacterCreatorModule::load() {
    const std::string path = savePath();
    std::ifstream in(path, std::ios::binary);
    if (!in) return setStatus("No saved character yet");
    std::stringstream text;
    text << in.rdbuf();
    HumanLook l;
    std::string error;
    if (!lookFromJson(text.str(), l, &error)) return setStatus("The save can't be read: " + error);
    setLook(l);
    setStatus("Loaded");
}
Missing Error Handling for Hair Placement

In hairOnHuman, if the hairstyle fails to place any roots on the scalp (e.g., due to a bad style definition or a malformed head shape), the function returns false without cleaning up the desc object. The caller receives a failure signal but may not know if the hair data was partially modified, potentially leading to inconsistent state in the character creator module.

bool hairOnHuman(HairDesc& desc, const std::string& style, const HumanPack& pack, const std::vector<glm::vec3>& shaped, int guides) {
    const HumanScalp scalp = findScalp(pack, shaped);
    if (scalp.verts.empty()) return false;
    // The rest pose lies over a sphere: make it the one round the whole
    // scalp, so strands start out over the skin, not in it.
    float outer = 0.0f;
    for (uint32_t v : scalp.verts) outer = std::max(outer, glm::distance(shaped[v], scalp.centre));
    if (!hairstyleOnHead(desc, style, scalp.centre, outer, guides, scalp.up, scalp.front)) return false;
    // A real face stands out in front of that sphere: hair combed back
    // (the loose styles) starts back and up from the hairline, or it would
    // fall over the eyes.
    if (glm::dot(desc.comb, scalp.front) < 0.0f) {
        desc.comb = -scalp.front * 1.2f;
        for (glm::vec3& d : desc.directions)
            if (const float ahead = glm::dot(d, scalp.front); ahead > 0.0f)
                d = glm::normalize(d + (scalp.up * 0.8f - scalp.front * 0.9f) * ahead);
    }

    // Each root onto the skin straight under it: the ray from the centre
    // through it, against the faces hair grows on (all four corners on the
    // scalp). No face there: no scalp (the face, the ears), dropped.
    const std::set<uint32_t> on(scalp.verts.begin(), scalp.verts.end());
    std::vector<std::array<glm::vec3, 3>> tris;
    const HumanGroup& g = pack.groups()[static_cast<size_t>(pack.findGroup("body"))];
    for (uint32_t f = g.firstFace; f < g.firstFace + g.faceCount; ++f) {
        const auto& q = pack.quads()[f].verts;
        if (!on.count(q[0]) || !on.count(q[1]) || !on.count(q[2]) || !on.count(q[3])) continue;
        tris.push_back({ shaped[q[0]], shaped[q[1]], shaped[q[2]] });
        if (q[3] != q[2]) tris.push_back({ shaped[q[0]], shaped[q[2]], shaped[q[3]] });
    }
    std::vector<size_t> keep;
    for (size_t r = 0; r < desc.roots.size(); ++r) {
        const glm::vec3 d = glm::normalize(desc.roots[r] - scalp.centre);
        float best = 1e9f;
        for (const auto& t : tris) { // Moller-Trumbore, either side
            const glm::vec3 e1 = t[1] - t[0], e2 = t[2] - t[0], p = glm::cross(d, e2);
            const float det = glm::dot(e1, p);
            if (std::abs(det) < 1e-12f) continue;
            const glm::vec3 s0 = scalp.centre - t[0];
            const float u = glm::dot(s0, p) / det;
            if (u < 0.0f || u > 1.0f) continue;
            const glm::vec3 qv = glm::cross(s0, e1);
            const float v = glm::dot(d, qv) / det;
            if (v < 0.0f || u + v > 1.0f) continue;
            const float hit = glm::dot(e2, qv) / det;
            if (hit > 0.0f) best = std::min(best, hit);
        }
        if (best > 1e8f) continue;
        desc.roots[r] = scalp.centre + d * (best + 0.0015f); // just over the skin
        keep.push_back(r);
    }
    if (keep.empty()) return false;
    auto pick = [&](auto& list) {
        if (list.size() != desc.roots.size()) return;
        std::remove_reference_t<decltype(list)> out;
        out.reserve(keep.size());
        for (size_t r : keep) out.push_back(list[r]);
        list = std::move(out);
    };
    pick(desc.directions);
    pick(desc.lengths);
    pick(desc.tiedTo);
    pick(desc.roots);
    return true;
}
Potential Integer Overflow in Vertex Count

The script checks if vertex count exceeds 0xFFFF (line 267) and raises an error. However, it does not validate that the number of morph targets or modifiers fits within reasonable limits that could cause memory issues or integer overflows elsewhere in the pack format (e.g., in HumanPack::parse where arrays are allocated based on counts). While the current check prevents immediate crash, adding bounds checks for target count and modifier count would make the parser more robust against malformed packs.

def cook(data_dir, source=None, log=print):
    positions, uvs, faces, groups = read_obj(os.path.join(data_dir, "3dobjs", "base.obj"))
    nverts = len(positions)
    if nverts > 0xFFFF:
        raise ValueError(f"{nverts} vertices: the pack stores vertex numbers in 16 bits")

Hardcoded Base64 Alphabet

The kB64 array on line 40 defines a custom base64 alphabet using - and _. While this is standard for URL-safe base64, the code does not explicitly handle cases where the input string might contain characters outside this set during decoding (unbase64url). Although the loop checks if (!p || c == '\0'), relying on std::strchr on a hardcoded array without explicit documentation of the allowed character set could lead to confusion or subtle bugs if the alphabet changes or is misused.

const char kB64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

std::string base64url(const std::vector<uint8_t>& in) {
    std::string out;
    for (size_t i = 0; i < in.size(); i += 3) {
        const uint32_t n = (uint32_t(in[i]) << 16) | (i + 1 < in.size() ? uint32_t(in[i + 1]) << 8 : 0) |
                           (i + 2 < in.size() ? in[i + 2] : 0);
        out += kB64[(n >> 18) & 63];
        out += kB64[(n >> 12) & 63];
        if (i + 1 < in.size()) out += kB64[(n >> 6) & 63];
        if (i + 2 < in.size()) out += kB64[n & 63];
    }
    return out;
}

ℹ️ Chunked review: the diff exceeded the model token budget, so it was reviewed in 3 chunks and the per-chunk results were merged.

## PR Reviewer Guide 🔍 <!-- pr-agent:review:full --> 📋 **Focus areas:** 🔴 6 open · ✅ 0 handled. Every push re-checks them; a handled area moves to *Resolved findings* at the bottom. <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 4 🔵🔵🔵🔵⚪</td></tr> <tr><td>🧪&nbsp;<strong>PR contains tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> <tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br> <details><summary><a href='https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/modules/CharacterCreatorModule.cpp#L763-L769'><strong>Missing Error Handling for File Operations</strong></a> The `save()` function (lines 763-769) opens a file for writing without checking if the path is valid or if the file was successfully opened before attempting to write. If `std::ofstream` fails to open the file (e.g., due to permissions, disk full, or invalid path), the code proceeds to write JSON to a potentially invalid stream object, which will fail silently or corrupt data. The function should check if the stream is good after opening and report an error status if it fails. </summary> ```c++ void CharacterCreatorModule::save() { const std::string path = savePath(); if (path.empty()) return setStatus("This device has no save folder"); std::ofstream out(path, std::ios::binary); out << lookToJson(look()); setStatus(out ? "Saved" : "Couldn't save to " + path); } ``` </details> <details><summary><a href='https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/modules/CharacterCreatorModule.cpp#L784-L792'><strong>Potential Memory Leak on Shutdown</strong></a> In the `shutdown()` function (lines 784-792), `m_hairDrawn` is retired but not explicitly reset to `nullptr`. While the `retire` call likely handles cleanup, failing to nullify the pointer could lead to dangling references if the module is re-initialized or if other parts of the code expect a null pointer after shutdown. It is best practice to set `m_hairDrawn = nullptr;` after retiring. </summary> ```c++ void CharacterCreatorModule::shutdown() { if (m_hairDrawn) m_app->renderer().retire(std::move(m_hairDrawn)); m_world.reset(); if (m_doc) { m_doc->Close(); m_doc = nullptr; } if (m_ui && m_ui->context()) m_ui->context()->RemoveDataModel("creator"); } ``` </details> <details><summary><a href='https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/modules/CharacterCreatorModule.cpp#L771-L782'><strong></strong></a> The `load()` function (lines 771-782) reads a file into a stringstream and then parses it. If the file is empty or contains invalid JSON, `lookFromJson` returns false and sets an error message in `status`, but the function does not explicitly clear any previous state or handle the case where the file exists but is unreadable (e.g., permission denied). Additionally, if `path.empty()` is true (no save folder), the function returns early without setting a status message, leaving the user unaware that the load operation was skipped. </summary> ```c++ void CharacterCreatorModule::load() { const std::string path = savePath(); std::ifstream in(path, std::ios::binary); if (!in) return setStatus("No saved character yet"); std::stringstream text; text << in.rdbuf(); HumanLook l; std::string error; if (!lookFromJson(text.str(), l, &error)) return setStatus("The save can't be read: " + error); setLook(l); setStatus("Loaded"); } ``` </details> <details><summary><a href='https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/HumanHair.cpp#L100-L164'><strong>Missing Error Handling for Hair Placement</strong></a> In `hairOnHuman`, if the hairstyle fails to place any roots on the scalp (e.g., due to a bad style definition or a malformed head shape), the function returns `false` without cleaning up the `desc` object. The caller receives a failure signal but may not know if the hair data was partially modified, potentially leading to inconsistent state in the character creator module. </summary> ```c++ bool hairOnHuman(HairDesc& desc, const std::string& style, const HumanPack& pack, const std::vector<glm::vec3>& shaped, int guides) { const HumanScalp scalp = findScalp(pack, shaped); if (scalp.verts.empty()) return false; // The rest pose lies over a sphere: make it the one round the whole // scalp, so strands start out over the skin, not in it. float outer = 0.0f; for (uint32_t v : scalp.verts) outer = std::max(outer, glm::distance(shaped[v], scalp.centre)); if (!hairstyleOnHead(desc, style, scalp.centre, outer, guides, scalp.up, scalp.front)) return false; // A real face stands out in front of that sphere: hair combed back // (the loose styles) starts back and up from the hairline, or it would // fall over the eyes. if (glm::dot(desc.comb, scalp.front) < 0.0f) { desc.comb = -scalp.front * 1.2f; for (glm::vec3& d : desc.directions) if (const float ahead = glm::dot(d, scalp.front); ahead > 0.0f) d = glm::normalize(d + (scalp.up * 0.8f - scalp.front * 0.9f) * ahead); } // Each root onto the skin straight under it: the ray from the centre // through it, against the faces hair grows on (all four corners on the // scalp). No face there: no scalp (the face, the ears), dropped. const std::set<uint32_t> on(scalp.verts.begin(), scalp.verts.end()); std::vector<std::array<glm::vec3, 3>> tris; const HumanGroup& g = pack.groups()[static_cast<size_t>(pack.findGroup("body"))]; for (uint32_t f = g.firstFace; f < g.firstFace + g.faceCount; ++f) { const auto& q = pack.quads()[f].verts; if (!on.count(q[0]) || !on.count(q[1]) || !on.count(q[2]) || !on.count(q[3])) continue; tris.push_back({ shaped[q[0]], shaped[q[1]], shaped[q[2]] }); if (q[3] != q[2]) tris.push_back({ shaped[q[0]], shaped[q[2]], shaped[q[3]] }); } std::vector<size_t> keep; for (size_t r = 0; r < desc.roots.size(); ++r) { const glm::vec3 d = glm::normalize(desc.roots[r] - scalp.centre); float best = 1e9f; for (const auto& t : tris) { // Moller-Trumbore, either side const glm::vec3 e1 = t[1] - t[0], e2 = t[2] - t[0], p = glm::cross(d, e2); const float det = glm::dot(e1, p); if (std::abs(det) < 1e-12f) continue; const glm::vec3 s0 = scalp.centre - t[0]; const float u = glm::dot(s0, p) / det; if (u < 0.0f || u > 1.0f) continue; const glm::vec3 qv = glm::cross(s0, e1); const float v = glm::dot(d, qv) / det; if (v < 0.0f || u + v > 1.0f) continue; const float hit = glm::dot(e2, qv) / det; if (hit > 0.0f) best = std::min(best, hit); } if (best > 1e8f) continue; desc.roots[r] = scalp.centre + d * (best + 0.0015f); // just over the skin keep.push_back(r); } if (keep.empty()) return false; auto pick = [&](auto& list) { if (list.size() != desc.roots.size()) return; std::remove_reference_t<decltype(list)> out; out.reserve(keep.size()); for (size_t r : keep) out.push_back(list[r]); list = std::move(out); }; pick(desc.directions); pick(desc.lengths); pick(desc.tiedTo); pick(desc.roots); return true; } ``` </details> <details><summary><a href='https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/tools/makehuman/cook_human.py#L263-L268'><strong>Potential Integer Overflow in Vertex Count</strong></a> The script checks if vertex count exceeds 0xFFFF (line 267) and raises an error. However, it does not validate that the number of morph targets or modifiers fits within reasonable limits that could cause memory issues or integer overflows elsewhere in the pack format (e.g., in `HumanPack::parse` where arrays are allocated based on counts). While the current check prevents immediate crash, adding bounds checks for target count and modifier count would make the parser more robust against malformed packs. </summary> ```python def cook(data_dir, source=None, log=print): positions, uvs, faces, groups = read_obj(os.path.join(data_dir, "3dobjs", "base.obj")) nverts = len(positions) if nverts > 0xFFFF: raise ValueError(f"{nverts} vertices: the pack stores vertex numbers in 16 bits") ``` </details> <details><summary><a href='https://git.kreative-kompas.com/khyretos/kk-engine/src/branch/refs/pull/120/head/engine/src/HumanLook.cpp#L40-L53'><strong>Hardcoded Base64 Alphabet</strong></a> The `kB64` array on line 40 defines a custom base64 alphabet using `-` and `_`. While this is standard for URL-safe base64, the code does not explicitly handle cases where the input string might contain characters outside this set during decoding (`unbase64url`). Although the loop checks `if (!p || c == '\0')`, relying on `std::strchr` on a hardcoded array without explicit documentation of the allowed character set could lead to confusion or subtle bugs if the alphabet changes or is misused. </summary> ```c++ const char kB64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; std::string base64url(const std::vector<uint8_t>& in) { std::string out; for (size_t i = 0; i < in.size(); i += 3) { const uint32_t n = (uint32_t(in[i]) << 16) | (i + 1 < in.size() ? uint32_t(in[i + 1]) << 8 : 0) | (i + 2 < in.size() ? in[i + 2] : 0); out += kB64[(n >> 18) & 63]; out += kB64[(n >> 12) & 63]; if (i + 1 < in.size()) out += kB64[(n >> 6) & 63]; if (i + 2 < in.size()) out += kB64[n & 63]; } return out; } ``` </details> </td></tr> </table> <hr> ℹ️ **Chunked review:** the diff exceeded the model token budget, so it was reviewed in 3 chunks and the per-chunk results were merged. <!-- pr-agent-review-state:v1 {"findings":[{"body":"**Potential Integer Overflow in Vertex Count**\n\nThe script checks if vertex count exceeds 0xFFFF (line 267) and raises an error. However, it does not validate that the number of morph targets or modifiers fits within reasonable limits that could cause memory issues or integer overflows elsewhere in the pack format (e.g., in `HumanPack::parse` where arrays are allocated based on counts). While the current check prevents immediate crash, adding bounds checks for target count and modifier count would make the parser more robust against malformed packs.","finding_id":"0bd79646d384","first_seen":"2026-10-25T03:45:08.022742Z","last_seen":"2026-10-25T03:45:08.022742Z","last_seen_head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","line_end":268,"line_start":263,"path":"tools/makehuman/cook_human.py","state":"ACTIVE"},{"body":"**Missing Error Handling for File Operations**\n\nThe `save()` function (lines 763-769) opens a file for writing without checking if the path is valid or if the file was successfully opened before attempting to write. If `std::ofstream` fails to open the file (e.g., due to permissions, disk full, or invalid path), the code proceeds to write JSON to a potentially invalid stream object, which will fail silently or corrupt data. The function should check if the stream is good after opening and report an error status if it fails.","finding_id":"2f5aa262c3fa","first_seen":"2026-10-25T03:45:08.022742Z","last_seen":"2026-10-25T03:45:08.022742Z","last_seen_head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","line_end":769,"line_start":763,"path":"engine/src/modules/CharacterCreatorModule.cpp","state":"ACTIVE"},{"body":"**Missing Error Handling for Hair Placement**\n\nIn `hairOnHuman`, if the hairstyle fails to place any roots on the scalp (e.g., due to a bad style definition or a malformed head shape), the function returns `false` without cleaning up the `desc` object. The caller receives a failure signal but may not know if the hair data was partially modified, potentially leading to inconsistent state in the character creator module.","finding_id":"50a1e3907a5f","first_seen":"2026-10-25T03:45:08.022742Z","last_seen":"2026-10-25T03:45:08.022742Z","last_seen_head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","line_end":164,"line_start":100,"path":"engine/src/HumanHair.cpp","state":"ACTIVE"},{"body":"**Potential Memory Leak on Shutdown**\n\nIn the `shutdown()` function (lines 784-792), `m_hairDrawn` is retired but not explicitly reset to `nullptr`. While the `retire` call likely handles cleanup, failing to nullify the pointer could lead to dangling references if the module is re-initialized or if other parts of the code expect a null pointer after shutdown. It is best practice to set `m_hairDrawn = nullptr;` after retiring.","finding_id":"6bb144bd17cc","first_seen":"2026-10-25T03:45:08.022742Z","last_seen":"2026-10-25T03:45:08.022742Z","last_seen_head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","line_end":792,"line_start":784,"path":"engine/src/modules/CharacterCreatorModule.cpp","state":"ACTIVE"},{"body":"The `load()` function (lines 771-782) reads a file into a stringstream and then parses it. If the file is empty or contains invalid JSON, `lookFromJson` returns false and sets an error message in `status`, but the function does not explicitly clear any previous state or handle the case where the file exists but is unreadable (e.g., permission denied). Additionally, if `path.empty()` is true (no save folder), the function returns early without setting a status message, leaving the user unaware that the load operation was skipped.","finding_id":"939cac5e5acf","first_seen":"2026-10-25T03:45:08.022742Z","last_seen":"2026-10-25T03:45:08.022742Z","last_seen_head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","line_end":782,"line_start":771,"path":"engine/src/modules/CharacterCreatorModule.cpp","state":"ACTIVE"},{"body":"**Hardcoded Base64 Alphabet**\n\nThe `kB64` array on line 40 defines a custom base64 alphabet using `-` and `_`. While this is standard for URL-safe base64, the code does not explicitly handle cases where the input string might contain characters outside this set during decoding (`unbase64url`). Although the loop checks `if (!p || c == '\\0')`, relying on `std::strchr` on a hardcoded array without explicit documentation of the allowed character set could lead to confusion or subtle bugs if the alphabet changes or is misused.","finding_id":"9891ff8dc03a","first_seen":"2026-10-25T03:45:08.022742Z","last_seen":"2026-10-25T03:45:08.022742Z","last_seen_head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","line_end":53,"line_start":40,"path":"engine/src/HumanLook.cpp","state":"ACTIVE"}],"last_run":{"complete":false,"excluded_files":[],"head_sha":"8c8a6892991949d924e4a2823d0e15c212465362","kind":"partial","run_id":"https://git.kreative-kompas.com/khyretos/kk-engine/commit/8c8a6892991949d924e4a2823d0e15c212465362"},"schema_version":1} -->
Collaborator

PR Code Suggestions ✨

No code suggestions found for the PR.

## PR Code Suggestions ✨ <!-- pr-agent:improve:no-suggestions --> No code suggestions found for the PR.

🧹 Lint report · ❌ 38 problems in 6 checks · 🧹 3 formatting checks to tidy

Nothing here blocks the merge (report only).

Check Errors Warnings Files
❌ MARKDOWN · markdownlint 24 0 252
❌ CPP · cppcheck 9 0 907
❌ YAML · yamllint 2 0 56
❌ BASH · shellcheck 1 0 11
❌ DOCKERFILE · hadolint 1 0 9
❌ REPOSITORY · trivy 1 0 –
MARKDOWN · markdownlint: first lines of the output
See documentation on https://megalinter.io/9.4.0/descriptors/markdown_markdownlint/
-----------------------------------------------
❌ [ERROR] for workspace /workspace/khyretos/kk-engine
Linter raw log:
docs/ANDROID.md:87 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/ANDROID.md:128 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/ANDROID.md:136 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/ANDROID.md:149 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/ANDROID.md:163 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/ANDROID.md:186 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/HISTORY.md:21 error MD025/single-title/single-h1 Multiple top-level headings in the same document [Context: " Kreative Kompas Engine (KKE)"]
docs/HISTORY.md:520 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/HISTORY.md:727 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/HISTORY.md:734 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/HISTORY.md:2031 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/HISTORY.md:2090 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/HUMANS.md:167 error MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "1) with MakeHuman's slider nam..."]
docs/MODDING.md:13 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/NETWORKING.md:114 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/PLATFORMS.md:48 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/PROCEDURAL_ANIMATION.md:36 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/PROCEDURAL_ANIMATION.md:284 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
games/racing/README.md:34 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
games/racing/README.md:76:91 error MD056/table-column-count Table column count [Expected: 4; Actual: 3; Too few cells, row will be missing data]
games/racing/README.md:77:89 error MD056/table-column-count Table column count [Expected: 4; Actual: 3; Too few cells, row will be missing data]
…
CPP · cppcheck: first lines of the output
See documentation on https://megalinter.io/9.4.0/descriptors/cpp_cppcheck/
-----------------------------------------------
✅ [SUCCESS] benchmarks/kke_bench/main.cpp
✅ [SUCCESS] benchmarks/kke_benchmark/main.cpp
✅ [SUCCESS] engine/include/engine/Buffer.h
✅ [SUCCESS] engine/include/engine/Mesh.h
✅ [SUCCESS] engine/include/engine/Pipeline.h
✅ [SUCCESS] engine/include/engine/Renderer.h
✅ [SUCCESS] engine/include/engine/SwapChain.h
✅ [SUCCESS] engine/include/engine/VulkanCheck.h
✅ [SUCCESS] engine/include/engine/VulkanDevice.h
✅ [SUCCESS] engine/include/engine/Window.h
✅ [SUCCESS] engine/include/kke/AnimRig.h
✅ [SUCCESS] engine/include/kke/Animator.h
✅ [SUCCESS] engine/include/kke/Application.h
✅ [SUCCESS] engine/include/kke/AssetCatalog.h
✅ [SUCCESS] engine/include/kke/AudioMixer.h
✅ [SUCCESS] engine/include/kke/AutoRig.h
✅ [SUCCESS] engine/include/kke/BenchRecorder.h
✅ [SUCCESS] engine/include/kke/BenchmarkReport.h
✅ [SUCCESS] engine/include/kke/Bloom.h
✅ [SUCCESS] engine/include/kke/BodyShape.h
✅ [SUCCESS] engine/include/kke/BreakGraph.h
✅ [SUCCESS] engine/include/kke/Breakables.h
✅ [SUCCESS] engine/include/kke/Buffer.h
…
YAML · yamllint: first lines of the output
See documentation on https://megalinter.io/9.4.0/descriptors/yaml_yamllint/
-----------------------------------------------
❌ [ERROR] for workspace /workspace/khyretos/kk-engine
Linter raw log:
games/goblin_horde/data/heroes.yml
  53:11     error    too many spaces after colon  (colons)
  63:11     error    too many spaces after colon  (colons)
BASH · shellcheck: first lines of the output
See documentation on https://megalinter.io/9.4.0/descriptors/bash_shellcheck/
-----------------------------------------------
❌ [ERROR] for workspace /workspace/khyretos/kk-engine
Linter raw log:
In tools/packaging/package.sh line 130:
	done < <(find "$stage" "$stage/benchmark" -maxdepth 1 -type f \( -name "*$exe" -o -name '*.dll' \) $([ -z "$exe" ] && echo "-perm -u+x") -print0)
                                                                                                           ^-- SC2046 (warning): Quote this to prevent word splitting.
For more information:
  https://www.shellcheck.net/wiki/SC2046 -- Quote this to prevent word splitt...
DOCKERFILE · hadolint: first lines of the output
See documentation on https://megalinter.io/9.4.0/descriptors/dockerfile_hadolint/
-----------------------------------------------
❌ [ERROR] for workspace /workspace/khyretos/kk-engine
Linter raw log:
docker/android.Dockerfile:19 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
docker/appimage.Dockerfile:38 SC2016 info: Expressions don't expand in single quotes, use double quotes for that.
docker/appimage.Dockerfile:51 DL3003 warning: Use WORKDIR to switch to a directory
docker/appimage.Dockerfile:57 DL3003 warning: Use WORKDIR to switch to a directory
docker/appimage.Dockerfile:63 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
docker/appimage.Dockerfile:63 DL3003 warning: Use WORKDIR to switch to a directory
docker/appimage.Dockerfile:73 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
docker/appimage.Dockerfile:73 DL3003 warning: Use WORKDIR to switch to a directory
docker/ci.Dockerfile:42 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
docker/ci.Dockerfile:56 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
docker/server/Dockerfile:22 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
docker/vm.Dockerfile:22 SC2086 info: Double quote to prevent globbing and word splitting.
docker/vm.Dockerfile:29 DL3003 warning: Use WORKDIR to switch to a directory
docker/vm.Dockerfile:29 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
website/Dockerfile:9 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
REPOSITORY · trivy: first lines of the output
See documentation on https://megalinter.io/9.4.0/descriptors/repository_trivy/
-----------------------------------------------
❌ [ERROR] for workspace /workspace/khyretos/kk-engine
Linter raw log:
2026-10-25T13:36:48Z	INFO	[vulndb] Need to update DB
2026-10-25T13:36:48Z	INFO	[vulndb] Downloading vulnerability DB...
2026-10-25T13:36:48Z	INFO	[vulndb] Downloading artifact...	repo="mirror.gcr.io/aquasec/trivy-db:2"
5.34 MiB / 129.84 MiB [-->___________________________________________________________] 4.11% ? p/s ?11.25 MiB / 129.84 MiB [----->_______________________________________________________] 8.66% ? p/s ?23.09 MiB / 129.84 MiB [---------->_________________________________________________] 17.78% ? p/s ?33.86 MiB / 129.84 MiB [------------>__________________________________] 26.08% 47.50 MiB p/s ETA 2s36.39 MiB / 129.84 MiB [------------->_________________________________] 28.03% 47.50 MiB p/s ETA 1s43.17 MiB / 129.84 MiB [--------------->_______________________________] 33.25% 47.50 MiB p/s ETA 1s49.28 MiB / 129.84 MiB [----------------->_____________________________] 37.95% 46.09 MiB p/s ETA 1s54.21 MiB / 129.84 MiB [------------------->___________________________] 41.76% 46.09 MiB p/s ETA 1s65.28 MiB / 129.84 MiB [----------------------->_______________________] 50.28% 46.09 MiB p/s ETA 1s74.67 MiB / 129.84 MiB [--------------------------->___________________] 57.51% 45.86 MiB p/s ETA 1s81.60 MiB / 129.84 MiB [----------------------------->_________________] 62.85% 45.86 MiB p/s ETA 1s86.48 MiB / 129.84 MiB [------------------------------->_______________] 66.61% 45.86 MiB p/s ETA 0s95.76 MiB / 129.84 MiB [---------------------------------->____________] 73.75% 45.18 MiB p/s ETA 0s100.78 MiB / 129.84 MiB [----------------------------------->__________] 77.62% 45.18 MiB p/s ETA 0s102.37 MiB / 129.84 MiB [------------------------------------>_________] 78.85% 45.18 MiB p/s ETA 0s105.37 MiB / 129.84 MiB [------------------------------------->________] 81.16% 43.30 MiB p/s ETA 0s111.06 MiB / 129.84 MiB [--------------------------------------->______] 85.54% 43.30 MiB p/s ETA 0s114.90 MiB / 129.84 MiB [---------------------------------------->_____] 88.50% 43.30 MiB p/s ETA 0s118.22 MiB / 129.84 MiB [----------------------------------------->____] 91.05% 41.87 MiB p/s ETA 0s122.53 MiB / 129.84 MiB [------------------------------------------->__] 94.37% 41.87 MiB p/s ETA 0s126.34 MiB / 129.84 MiB [-------------------------------------------->_] 97.31% 41.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 40.44 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 40.44 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 40.44 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 37.83 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 37.83 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 37.83 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 35.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 35.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 35.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 33.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 33.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 33.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 30.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 30.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 30.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 28.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 28.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 28.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 27.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 27.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 27.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 25.35 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 25.35 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 25.35 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 23.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 23.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 23.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 22.19 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 22.19 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 22.19 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 20.76 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 20.76 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 20.76 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 19.42 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 19.42 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 19.42 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 18.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 18.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 18.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 16.99 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 16.99 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 16.99 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 15.90 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 15.90 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 15.90 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 14.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 14.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 14.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.91 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.91 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.91 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.01 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.01 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.01 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 12.17 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 12.17 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 12.17 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 11.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 11.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 11.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 10.65 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 10.65 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 10.65 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.32 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.32 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.32 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.63 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.63 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.63 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.14 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------------] 100.00% 6.51 MiB p/s 20s2026-10-25T13:37:09Z	INFO	[vulndb] Artifact successfully downloaded	repo="mirror.gcr.io/aquasec/trivy-db:2"
2026-10-25T13:37:09Z	INFO	[vuln] Vulnerability scanning is enabled
2026-10-25T13:37:09Z	INFO	[misconfig] Misconfiguration scanning is enabled
2026-10-25T13:37:09Z	INFO	[checks-client] Need to update the checks bundle
2026-10-25T13:37:09Z	INFO	[checks-client] Downloading the checks bundle...
234.57 KiB / 234.57 KiB [------------------------------------------------------] 100.00% ? p/s 200ms2026-10-25T13:37:12Z	WARN	[rego] Module will be skipped as current version of Trivy is older than minimum trivy version required - please update Trivy to use this module	file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/cloudfront/enable_logging.rego" minimum_trivy_version="0.72.0"
2026-10-25T13:37:13Z	ERROR	[dockerfile scanner] Failed to parse file	file_path="docker/server/Dockerfile.dockerignore" err="parse dockerfile instruction: parse instruction \".git\": unknown instruction: .git"
2026-10-25T13:37:14Z	INFO	Number of language-specific files	num=0
2026-10-25T13:37:14Z	INFO	Detected config files	num=9
Report Summary
┌──────────────────────────────┬────────────┬─────────────────┬───────────────────┐
│            Target            │    Type    │ Vulnerabilities │ Misconfigurations │
├──────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ benchmarks/docker/Dockerfile │ dockerfile │        -        │         2         │
├──────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ docker/android.Dockerfile    │ dockerfile │        -        │         2         │
├──────────────────────────────┼────────────┼─────────────────┼───────────────────┤
│ docker/appimage.Dockerfile   │ dockerfile │        -        │         5         │
…

🧹 Formatting only (layout, no behaviour; the nightly lint-fix job applies these as a PR): YAML · prettier (56 files checked), BASH · shfmt (11 files checked), JSON · prettier (147 files checked)

✅ Clean: actionlint, git_diff, gitleaks, grype, jsonlint, secretlint, stylelint, trufflehog, xmllint

MegaLinter on soucouyant · full logs in the Actions run · comment /improve for AI fix suggestions

<!-- kk-lint --> ## 🧹 Lint report · ❌ 38 problems in 6 checks · 🧹 3 formatting checks to tidy Nothing here blocks the merge (report only). | | Check | Errors | Warnings | Files | |---|---|---:|---:|---:| | ❌ | MARKDOWN · markdownlint | 24 | 0 | 252 | | ❌ | CPP · cppcheck | 9 | 0 | 907 | | ❌ | YAML · yamllint | 2 | 0 | 56 | | ❌ | BASH · shellcheck | 1 | 0 | 11 | | ❌ | DOCKERFILE · hadolint | 1 | 0 | 9 | | ❌ | REPOSITORY · trivy | 1 | 0 | – | <details><summary>MARKDOWN · markdownlint: first lines of the output</summary> ```text See documentation on https://megalinter.io/9.4.0/descriptors/markdown_markdownlint/ ----------------------------------------------- ❌ [ERROR] for workspace /workspace/khyretos/kk-engine Linter raw log: docs/ANDROID.md:87 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/ANDROID.md:128 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/ANDROID.md:136 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/ANDROID.md:149 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/ANDROID.md:163 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/ANDROID.md:186 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/HISTORY.md:21 error MD025/single-title/single-h1 Multiple top-level headings in the same document [Context: " Kreative Kompas Engine (KKE)"] docs/HISTORY.md:520 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/HISTORY.md:727 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/HISTORY.md:734 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/HISTORY.md:2031 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/HISTORY.md:2090 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/HUMANS.md:167 error MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "1) with MakeHuman's slider nam..."] docs/MODDING.md:13 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/NETWORKING.md:114 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/PLATFORMS.md:48 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/PROCEDURAL_ANIMATION.md:36 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] docs/PROCEDURAL_ANIMATION.md:284 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] games/racing/README.md:34 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] games/racing/README.md:76:91 error MD056/table-column-count Table column count [Expected: 4; Actual: 3; Too few cells, row will be missing data] games/racing/README.md:77:89 error MD056/table-column-count Table column count [Expected: 4; Actual: 3; Too few cells, row will be missing data] … ``` </details> <details><summary>CPP · cppcheck: first lines of the output</summary> ```text See documentation on https://megalinter.io/9.4.0/descriptors/cpp_cppcheck/ ----------------------------------------------- ✅ [SUCCESS] benchmarks/kke_bench/main.cpp ✅ [SUCCESS] benchmarks/kke_benchmark/main.cpp ✅ [SUCCESS] engine/include/engine/Buffer.h ✅ [SUCCESS] engine/include/engine/Mesh.h ✅ [SUCCESS] engine/include/engine/Pipeline.h ✅ [SUCCESS] engine/include/engine/Renderer.h ✅ [SUCCESS] engine/include/engine/SwapChain.h ✅ [SUCCESS] engine/include/engine/VulkanCheck.h ✅ [SUCCESS] engine/include/engine/VulkanDevice.h ✅ [SUCCESS] engine/include/engine/Window.h ✅ [SUCCESS] engine/include/kke/AnimRig.h ✅ [SUCCESS] engine/include/kke/Animator.h ✅ [SUCCESS] engine/include/kke/Application.h ✅ [SUCCESS] engine/include/kke/AssetCatalog.h ✅ [SUCCESS] engine/include/kke/AudioMixer.h ✅ [SUCCESS] engine/include/kke/AutoRig.h ✅ [SUCCESS] engine/include/kke/BenchRecorder.h ✅ [SUCCESS] engine/include/kke/BenchmarkReport.h ✅ [SUCCESS] engine/include/kke/Bloom.h ✅ [SUCCESS] engine/include/kke/BodyShape.h ✅ [SUCCESS] engine/include/kke/BreakGraph.h ✅ [SUCCESS] engine/include/kke/Breakables.h ✅ [SUCCESS] engine/include/kke/Buffer.h … ``` </details> <details><summary>YAML · yamllint: first lines of the output</summary> ```text See documentation on https://megalinter.io/9.4.0/descriptors/yaml_yamllint/ ----------------------------------------------- ❌ [ERROR] for workspace /workspace/khyretos/kk-engine Linter raw log: games/goblin_horde/data/heroes.yml 53:11 error too many spaces after colon (colons) 63:11 error too many spaces after colon (colons) ``` </details> <details><summary>BASH · shellcheck: first lines of the output</summary> ```text See documentation on https://megalinter.io/9.4.0/descriptors/bash_shellcheck/ ----------------------------------------------- ❌ [ERROR] for workspace /workspace/khyretos/kk-engine Linter raw log: In tools/packaging/package.sh line 130: done < <(find "$stage" "$stage/benchmark" -maxdepth 1 -type f \( -name "*$exe" -o -name '*.dll' \) $([ -z "$exe" ] && echo "-perm -u+x") -print0) ^-- SC2046 (warning): Quote this to prevent word splitting. For more information: https://www.shellcheck.net/wiki/SC2046 -- Quote this to prevent word splitt... ``` </details> <details><summary>DOCKERFILE · hadolint: first lines of the output</summary> ```text See documentation on https://megalinter.io/9.4.0/descriptors/dockerfile_hadolint/ ----------------------------------------------- ❌ [ERROR] for workspace /workspace/khyretos/kk-engine Linter raw log: docker/android.Dockerfile:19 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check docker/appimage.Dockerfile:38 SC2016 info: Expressions don't expand in single quotes, use double quotes for that. docker/appimage.Dockerfile:51 DL3003 warning: Use WORKDIR to switch to a directory docker/appimage.Dockerfile:57 DL3003 warning: Use WORKDIR to switch to a directory docker/appimage.Dockerfile:63 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check docker/appimage.Dockerfile:63 DL3003 warning: Use WORKDIR to switch to a directory docker/appimage.Dockerfile:73 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check docker/appimage.Dockerfile:73 DL3003 warning: Use WORKDIR to switch to a directory docker/ci.Dockerfile:42 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check docker/ci.Dockerfile:56 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check docker/server/Dockerfile:22 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check docker/vm.Dockerfile:22 SC2086 info: Double quote to prevent globbing and word splitting. docker/vm.Dockerfile:29 DL3003 warning: Use WORKDIR to switch to a directory docker/vm.Dockerfile:29 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check website/Dockerfile:9 DL4006 warning: Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check ``` </details> <details><summary>REPOSITORY · trivy: first lines of the output</summary> ```text See documentation on https://megalinter.io/9.4.0/descriptors/repository_trivy/ ----------------------------------------------- ❌ [ERROR] for workspace /workspace/khyretos/kk-engine Linter raw log: 2026-10-25T13:36:48Z INFO [vulndb] Need to update DB 2026-10-25T13:36:48Z INFO [vulndb] Downloading vulnerability DB... 2026-10-25T13:36:48Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2" 5.34 MiB / 129.84 MiB [-->___________________________________________________________] 4.11% ? p/s ?11.25 MiB / 129.84 MiB [----->_______________________________________________________] 8.66% ? p/s ?23.09 MiB / 129.84 MiB [---------->_________________________________________________] 17.78% ? p/s ?33.86 MiB / 129.84 MiB [------------>__________________________________] 26.08% 47.50 MiB p/s ETA 2s36.39 MiB / 129.84 MiB [------------->_________________________________] 28.03% 47.50 MiB p/s ETA 1s43.17 MiB / 129.84 MiB [--------------->_______________________________] 33.25% 47.50 MiB p/s ETA 1s49.28 MiB / 129.84 MiB [----------------->_____________________________] 37.95% 46.09 MiB p/s ETA 1s54.21 MiB / 129.84 MiB [------------------->___________________________] 41.76% 46.09 MiB p/s ETA 1s65.28 MiB / 129.84 MiB [----------------------->_______________________] 50.28% 46.09 MiB p/s ETA 1s74.67 MiB / 129.84 MiB [--------------------------->___________________] 57.51% 45.86 MiB p/s ETA 1s81.60 MiB / 129.84 MiB [----------------------------->_________________] 62.85% 45.86 MiB p/s ETA 1s86.48 MiB / 129.84 MiB [------------------------------->_______________] 66.61% 45.86 MiB p/s ETA 0s95.76 MiB / 129.84 MiB [---------------------------------->____________] 73.75% 45.18 MiB p/s ETA 0s100.78 MiB / 129.84 MiB [----------------------------------->__________] 77.62% 45.18 MiB p/s ETA 0s102.37 MiB / 129.84 MiB [------------------------------------>_________] 78.85% 45.18 MiB p/s ETA 0s105.37 MiB / 129.84 MiB [------------------------------------->________] 81.16% 43.30 MiB p/s ETA 0s111.06 MiB / 129.84 MiB [--------------------------------------->______] 85.54% 43.30 MiB p/s ETA 0s114.90 MiB / 129.84 MiB [---------------------------------------->_____] 88.50% 43.30 MiB p/s ETA 0s118.22 MiB / 129.84 MiB [----------------------------------------->____] 91.05% 41.87 MiB p/s ETA 0s122.53 MiB / 129.84 MiB [------------------------------------------->__] 94.37% 41.87 MiB p/s ETA 0s126.34 MiB / 129.84 MiB [-------------------------------------------->_] 97.31% 41.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 40.44 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 40.44 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 40.44 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 37.83 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 37.83 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 37.83 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 35.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 35.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 35.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 33.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 33.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 33.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 30.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 30.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 30.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 28.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 28.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 28.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 27.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 27.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 27.10 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 25.35 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 25.35 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 25.35 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 23.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 23.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 23.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 22.19 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 22.19 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 22.19 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 20.76 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 20.76 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 20.76 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 19.42 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 19.42 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 19.42 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 18.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 18.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 18.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 16.99 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 16.99 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 16.99 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 15.90 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 15.90 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 15.90 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 14.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 14.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 14.87 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.91 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.91 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.91 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.01 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.01 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 13.01 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 12.17 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 12.17 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 12.17 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 11.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 11.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 11.39 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 10.65 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 10.65 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------->] 100.00% 10.65 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.97 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.32 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.32 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 9.32 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.72 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 8.16 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.63 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.63 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.63 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [--------------------------------------------->] 100.00% 7.14 MiB p/s ETA 0s129.84 MiB / 129.84 MiB [-------------------------------------------------] 100.00% 6.51 MiB p/s 20s2026-10-25T13:37:09Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2" 2026-10-25T13:37:09Z INFO [vuln] Vulnerability scanning is enabled 2026-10-25T13:37:09Z INFO [misconfig] Misconfiguration scanning is enabled 2026-10-25T13:37:09Z INFO [checks-client] Need to update the checks bundle 2026-10-25T13:37:09Z INFO [checks-client] Downloading the checks bundle... 234.57 KiB / 234.57 KiB [------------------------------------------------------] 100.00% ? p/s 200ms2026-10-25T13:37:12Z WARN [rego] Module will be skipped as current version of Trivy is older than minimum trivy version required - please update Trivy to use this module file_path="root/.cache/trivy/policy/content/policies/cloud/policies/aws/cloudfront/enable_logging.rego" minimum_trivy_version="0.72.0" 2026-10-25T13:37:13Z ERROR [dockerfile scanner] Failed to parse file file_path="docker/server/Dockerfile.dockerignore" err="parse dockerfile instruction: parse instruction \".git\": unknown instruction: .git" 2026-10-25T13:37:14Z INFO Number of language-specific files num=0 2026-10-25T13:37:14Z INFO Detected config files num=9 Report Summary ┌──────────────────────────────┬────────────┬─────────────────┬───────────────────┐ │ Target │ Type │ Vulnerabilities │ Misconfigurations │ ├──────────────────────────────┼────────────┼─────────────────┼───────────────────┤ │ benchmarks/docker/Dockerfile │ dockerfile │ - │ 2 │ ├──────────────────────────────┼────────────┼─────────────────┼───────────────────┤ │ docker/android.Dockerfile │ dockerfile │ - │ 2 │ ├──────────────────────────────┼────────────┼─────────────────┼───────────────────┤ │ docker/appimage.Dockerfile │ dockerfile │ - │ 5 │ … ``` </details> 🧹 Formatting only (layout, no behaviour; the nightly lint-fix job applies these as a PR): YAML · prettier (56 files checked), BASH · shfmt (11 files checked), JSON · prettier (147 files checked) ✅ Clean: actionlint, git_diff, gitleaks, grype, jsonlint, secretlint, stylelint, trufflehog, xmllint <sub>MegaLinter on soucouyant · full logs in the Actions run · comment <code>/improve</code> for AI fix suggestions</sub>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
khyretos/kk-engine!120
No description provided.