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
("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
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 -
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
// build output over the base game data (repeated -data) renders the result
// 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
import (
@@ -43,6 +45,11 @@ type galleryEntry struct {
transparent bool
gifName 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
// winning layer isn't the first -data directory - the same content
// 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) 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
// manifest entry, bypassing hacked's own fileAllowlist
// (ss1/world/FileLoader.go): that check is only skipped when a file is
@@ -142,11 +163,13 @@ func main() {
// later layer (a mod) overrides.
sourceFileByKey := make(map[resource.Key]string)
sourceDirIndexByKey := make(map[resource.Key]int)
groupByDirIndex := make(map[int]string)
for dirIndex, dir := range dirs {
absDir, err := filepath.Abs(dir)
if err != nil {
log.Fatalf("resolving -data %q: %v", dir, err)
}
groupByDirIndex[dirIndex] = groupName(absDir, dirIndex)
entry, err := loadDataDir(absDir)
if err != nil {
// A mod that doesn't touch any resource file yet (e.g. an
@@ -234,6 +257,8 @@ func main() {
if !ok {
continue
}
entry.dirIndex = sourceDirIndexByKey[key]
entry.group = groupByDirIndex[entry.dirIndex]
// 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
@@ -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 .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">` +
`<label>Model: <select id="model">` +
`<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>` +
`<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>` +
`<button id="clear-filter" type="button">Clear filter</button>` +
`<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>` +
`</div>`)
@@ -680,17 +724,31 @@ func writeIndex(path string, entries []galleryEntry) error {
fmt.Fprintf(&b, "<h1>%d animated resource(s)</h1>", len(entries))
// Animation descriptors and multi-block bitmaps are genuinely distinct
// content types (different decode path, see contentTypeName) - grouped
// into separate sections rather than interleaved.
var animations, bitmaps []galleryEntry
// Cards are sectioned per -data layer (the base game, then each mod), and
// within one by content type: animation descriptors and multi-block
// bitmaps are genuinely distinct (different decode path, see
// contentTypeName).
type section struct {
group string
dirIndex int
animations, bitmaps []galleryEntry
}
var sections []*section
sectionByDir := make(map[int]*section)
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" {
animations = append(animations, e)
sec.animations = append(sec.animations, e)
} 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) {
for i, e := range group {
@@ -700,7 +758,7 @@ func writeIndex(path string, entries []galleryEntry) error {
}
search := strings.ToLower(strings.Join([]string{
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.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>")
}
}
fmt.Fprintf(&b, "<h2>Animations (%d)</h2>", len(animations))
writeCards(animations, "a")
fmt.Fprintf(&b, "<h2>Bitmaps (%d)</h2>", len(bitmaps))
writeCards(bitmaps, "b")
for _, sec := range sections {
b.WriteString("<div class=\"group\">")
fmt.Fprintf(&b, "<h2>%s (%d)</h2>", html.EscapeString(sec.group), len(sec.animations)+len(sec.bitmaps))
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>
var basket = document.getElementById('basket');
// 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.
var cardHomes = new Map();
Array.prototype.forEach.call(document.querySelectorAll('.card'), function (card) {
@@ -796,9 +863,24 @@ function applyFilter() {
card.style.display = match ? '' : 'none';
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('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();
</script>`)