This commit is contained in:
+4
-1
@@ -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.
|
||||||
|
|||||||
@@ -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/<name>/bitmaps/<label>.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/<name>/bitmaps/<label>.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 = §ion{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>`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user