Group animgallery cards by mod and add group quick filters
build / build (push) Successful in 2m49s

This commit is contained in:
ml
2026-10-27 08:27:59 +01:00
parent 90303894ce
commit 345f8b5f17
2 changed files with 99 additions and 14 deletions
+4 -1
View File
@@ -324,7 +324,10 @@ are layered this way, a card for any resource a later layer actually
overrides shows both versions side by side - the first directory's own overrides shows both versions side by side - the first directory's own
("base") and the winning one ("this build") - so a mod's effect is always ("base") and the winning one ("this build") - so a mod's effect is always
checked against the original it's replacing, not recalled from memory or a checked against the original it's replacing, not recalled from memory or a
separate base-only gallery run. separate base-only gallery run. Cards are sectioned by the `-data`
directory they come from - "base game" first, then one section per mod
(named after `dist/mods/<name>`) - so a mod's own animations, for example
all of `upscale_medbot`'s, are listed together.
`gen_animgallery.sh` is a from-scratch Go decoder, not the game engine - `gen_animgallery.sh` is a from-scratch Go decoder, not the game engine -
it never exercises the engine's own texture upload/caching code at all. it never exercises the engine's own texture upload/caching code at all.
+95 -13
View File
@@ -7,7 +7,9 @@
// browsed by eye instead of guessed at by resource ID. Layering a mod's own // browsed by eye instead of guessed at by resource ID. Layering a mod's own
// build output over the base game data (repeated -data) renders the result // build output over the base game data (repeated -data) renders the result
// as actually played - and for any resource the mod overrides, the base // as actually played - and for any resource the mod overrides, the base
// game's own version alongside it, for a direct comparison. // game's own version alongside it, for a direct comparison. The index page
// sections its cards by the -data layer they come from: the base game, then
// each mod.
package main package main
import ( import (
@@ -43,6 +45,11 @@ type galleryEntry struct {
transparent bool transparent bool
gifName string gifName string
filmstripName string filmstripName string
// group names the winning -data layer (see groupName), dirIndex is that
// layer's position among the -data directories - the index page
// sections its cards by group, in dirIndex order.
group string
dirIndex int
// baseGifName/baseFilmstripName are set only when this resource's // baseGifName/baseFilmstripName are set only when this resource's
// winning layer isn't the first -data directory - the same content // winning layer isn't the first -data directory - the same content
// rendered from that first (base) directory alone, for a side-by-side // rendered from that first (base) directory alone, for a side-by-side
@@ -61,6 +68,20 @@ type dataDirs []string
func (d *dataDirs) String() string { return strings.Join(*d, ",") } func (d *dataDirs) String() string { return strings.Join(*d, ",") }
func (d *dataDirs) Set(v string) error { *d = append(*d, v); return nil } func (d *dataDirs) Set(v string) error { *d = append(*d, v); return nil }
// groupName labels one -data layer for the index page: "base game" for the
// first one, otherwise the mod's name. A built mod lives at
// dist/mods/<name>/mod, so a directory named "mod" is labeled by its parent.
func groupName(absDir string, dirIndex int) string {
if dirIndex == 0 {
return "base game"
}
name := filepath.Base(absDir)
if name == "mod" {
name = filepath.Base(filepath.Dir(absDir))
}
return name
}
// loadDataDir merges every .res/.dat file directly inside dir into one // loadDataDir merges every .res/.dat file directly inside dir into one
// manifest entry, bypassing hacked's own fileAllowlist // manifest entry, bypassing hacked's own fileAllowlist
// (ss1/world/FileLoader.go): that check is only skipped when a file is // (ss1/world/FileLoader.go): that check is only skipped when a file is
@@ -142,11 +163,13 @@ func main() {
// later layer (a mod) overrides. // later layer (a mod) overrides.
sourceFileByKey := make(map[resource.Key]string) sourceFileByKey := make(map[resource.Key]string)
sourceDirIndexByKey := make(map[resource.Key]int) sourceDirIndexByKey := make(map[resource.Key]int)
groupByDirIndex := make(map[int]string)
for dirIndex, dir := range dirs { for dirIndex, dir := range dirs {
absDir, err := filepath.Abs(dir) absDir, err := filepath.Abs(dir)
if err != nil { if err != nil {
log.Fatalf("resolving -data %q: %v", dir, err) log.Fatalf("resolving -data %q: %v", dir, err)
} }
groupByDirIndex[dirIndex] = groupName(absDir, dirIndex)
entry, err := loadDataDir(absDir) entry, err := loadDataDir(absDir)
if err != nil { if err != nil {
// A mod that doesn't touch any resource file yet (e.g. an // A mod that doesn't touch any resource file yet (e.g. an
@@ -234,6 +257,8 @@ func main() {
if !ok { if !ok {
continue continue
} }
entry.dirIndex = sourceDirIndexByKey[key]
entry.group = groupByDirIndex[entry.dirIndex]
// A later layer wins this key over the first (base) -data directory // A later layer wins this key over the first (base) -data directory
// - also render the base directory's own version of it, so the two // - also render the base directory's own version of it, so the two
@@ -663,6 +688,23 @@ func writeIndex(path string, entries []galleryEntry) error {
b.WriteString("#basket:empty::before{content:'(nothing selected yet - check animations below to add them here)';color:#678;font-style:italic}") b.WriteString("#basket:empty::before{content:'(nothing selected yet - check animations below to add them here)';color:#678;font-style:italic}")
b.WriteString("#basket .card{background:#2a3a2a}</style></head><body>") b.WriteString("#basket .card{background:#2a3a2a}</style></head><body>")
// One quick-filter button per group, in -data order.
groupNames := make(map[int]string)
for _, e := range entries {
groupNames[e.dirIndex] = e.group
}
groupIndexes := make([]int, 0, len(groupNames))
for i := range groupNames {
groupIndexes = append(groupIndexes, i)
}
sort.Ints(groupIndexes)
groupButtons := `<br><span id="group-filters">Groups:`
for _, i := range groupIndexes {
name := html.EscapeString(groupNames[i])
groupButtons += fmt.Sprintf(` <button type="button" class="group-filter" data-filter="%s">%s</button>`, name, name)
}
groupButtons += `</span>`
b.WriteString(`<div id="controls">` + b.WriteString(`<div id="controls">` +
`<label>Model: <select id="model">` + `<label>Model: <select id="model">` +
`<option>realesrgan-x4plus-anime</option>` + `<option>realesrgan-x4plus-anime</option>` +
@@ -672,7 +714,9 @@ func writeIndex(path string, entries []galleryEntry) error {
`<label>Scale: <input id="scale" type="number" value="2" min="1" max="4" style="width:4em"></label>` + `<label>Scale: <input id="scale" type="number" value="2" min="1" max="4" style="width:4em"></label>` +
`<button id="clear-sel" type="button">Clear selection</button>` + `<button id="clear-sel" type="button">Clear selection</button>` +
`<br><label>Filter: <input type="text" id="search" placeholder="id, language, file, transparent..." style="width:22em"></label>` + `<br><label>Filter: <input type="text" id="search" placeholder="id, language, file, transparent..." style="width:22em"></label>` +
`<button id="clear-filter" type="button">Clear filter</button>` +
`<span id="shown-count"></span>` + `<span id="shown-count"></span>` +
groupButtons +
`<textarea id="mdl-output" rows="12" readonly placeholder="Select animations below to generate an MDL upscale definition here - copy it into modding/mods/&lt;name&gt;/bitmaps/&lt;label&gt;.json"></textarea>` + `<textarea id="mdl-output" rows="12" readonly placeholder="Select animations below to generate an MDL upscale definition here - copy it into modding/mods/&lt;name&gt;/bitmaps/&lt;label&gt;.json"></textarea>` +
`</div>`) `</div>`)
@@ -680,17 +724,31 @@ func writeIndex(path string, entries []galleryEntry) error {
fmt.Fprintf(&b, "<h1>%d animated resource(s)</h1>", len(entries)) fmt.Fprintf(&b, "<h1>%d animated resource(s)</h1>", len(entries))
// Animation descriptors and multi-block bitmaps are genuinely distinct // Cards are sectioned per -data layer (the base game, then each mod), and
// content types (different decode path, see contentTypeName) - grouped // within one by content type: animation descriptors and multi-block
// into separate sections rather than interleaved. // bitmaps are genuinely distinct (different decode path, see
var animations, bitmaps []galleryEntry // contentTypeName).
type section struct {
group string
dirIndex int
animations, bitmaps []galleryEntry
}
var sections []*section
sectionByDir := make(map[int]*section)
for _, e := range entries { for _, e := range entries {
sec := sectionByDir[e.dirIndex]
if sec == nil {
sec = &section{group: e.group, dirIndex: e.dirIndex}
sectionByDir[e.dirIndex] = sec
sections = append(sections, sec)
}
if contentTypeName(e.kind) == "Animation" { if contentTypeName(e.kind) == "Animation" {
animations = append(animations, e) sec.animations = append(sec.animations, e)
} else { } else {
bitmaps = append(bitmaps, e) sec.bitmaps = append(sec.bitmaps, e)
} }
} }
sort.Slice(sections, func(i, j int) bool { return sections[i].dirIndex < sections[j].dirIndex })
writeCards := func(group []galleryEntry, idPrefix string) { writeCards := func(group []galleryEntry, idPrefix string) {
for i, e := range group { for i, e := range group {
@@ -700,7 +758,7 @@ func writeIndex(path string, entries []galleryEntry) error {
} }
search := strings.ToLower(strings.Join([]string{ search := strings.ToLower(strings.Join([]string{
fmt.Sprintf("0x%x", int(e.id)), fmt.Sprintf("%d", int(e.id)), fmt.Sprintf("0x%x", int(e.id)), fmt.Sprintf("%d", int(e.id)),
e.lang.String(), e.kind, e.sourceFile, e.lang.String(), e.kind, e.sourceFile, e.group,
fmt.Sprintf("%dx%d", e.width, e.height), transparency, fmt.Sprintf("%dx%d", e.width, e.height), transparency,
}, " ")) }, " "))
fmt.Fprintf(&b, "<div class=\"card\" data-search=\"%s\">", html.EscapeString(search)) fmt.Fprintf(&b, "<div class=\"card\" data-search=\"%s\">", html.EscapeString(search))
@@ -728,15 +786,24 @@ func writeIndex(path string, entries []galleryEntry) error {
b.WriteString("</div>") b.WriteString("</div>")
} }
} }
fmt.Fprintf(&b, "<h2>Animations (%d)</h2>", len(animations)) for _, sec := range sections {
writeCards(animations, "a") b.WriteString("<div class=\"group\">")
fmt.Fprintf(&b, "<h2>Bitmaps (%d)</h2>", len(bitmaps)) fmt.Fprintf(&b, "<h2>%s (%d)</h2>", html.EscapeString(sec.group), len(sec.animations)+len(sec.bitmaps))
writeCards(bitmaps, "b") if len(sec.animations) > 0 {
fmt.Fprintf(&b, "<h3>Animations (%d)</h3>", len(sec.animations))
writeCards(sec.animations, fmt.Sprintf("%da", sec.dirIndex))
}
if len(sec.bitmaps) > 0 {
fmt.Fprintf(&b, "<h3>Bitmaps (%d)</h3>", len(sec.bitmaps))
writeCards(sec.bitmaps, fmt.Sprintf("%db", sec.dirIndex))
}
b.WriteString("</div>")
}
b.WriteString(`<script> b.WriteString(`<script>
var basket = document.getElementById('basket'); var basket = document.getElementById('basket');
// Where each card lives when not selected, so unchecking it puts it back // Where each card lives when not selected, so unchecking it puts it back
// in its original spot (Animations/Bitmaps section, original order) // in its original spot (its group's Animations/Bitmaps section, original order)
// instead of just appending it back at the end. // instead of just appending it back at the end.
var cardHomes = new Map(); var cardHomes = new Map();
Array.prototype.forEach.call(document.querySelectorAll('.card'), function (card) { Array.prototype.forEach.call(document.querySelectorAll('.card'), function (card) {
@@ -796,9 +863,24 @@ function applyFilter() {
card.style.display = match ? '' : 'none'; card.style.display = match ? '' : 'none';
if (match) shown++; if (match) shown++;
}); });
// A group with no visible card left hides its headings too.
Array.prototype.forEach.call(document.querySelectorAll('.group'), function (group) {
var visible = Array.prototype.some.call(group.querySelectorAll('.card'), function (card) {
return card.style.display !== 'none';
});
group.style.display = visible ? '' : 'none';
});
document.getElementById('shown-count').textContent = shown + ' / ' + cards.length + ' shown'; document.getElementById('shown-count').textContent = shown + ' / ' + cards.length + ' shown';
} }
document.getElementById('search').addEventListener('input', applyFilter); document.getElementById('search').addEventListener('input', applyFilter);
function setFilter(text) {
document.getElementById('search').value = text;
applyFilter();
}
document.getElementById('clear-filter').addEventListener('click', function () { setFilter(''); });
Array.prototype.forEach.call(document.querySelectorAll('.group-filter'), function (btn) {
btn.addEventListener('click', function () { setFilter(btn.dataset.filter); });
});
applyFilter(); applyFilter();
</script>`) </script>`)