Skip to content

Commit cea8055

Browse files
TMHSDigitalclaude
andcommitted
engine filter: hide other engines' projects and plugins, reorder on every switch
The filter hid only licence-locked entries. It now also hides entries whose formats are native to another engine (Godot addons and projects, Unreal projects and plugins, Unity-only packages); an addon stays native whatever asset formats it also lists, and a pack with portable files stays listed. The card placement key now includes the engine, so switching engines after any other interaction reorders the list, and a ranked search breaks score ties by engine. The key moved to search.mjs (placementKey) with tests. Closes #171 Closes #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXRUqm7oDedPDk3QWtK
1 parent fee00d5 commit cea8055

4 files changed

Lines changed: 86 additions & 17 deletions

File tree

‎site/lib/lib.test.mjs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,25 @@ eq("filter: deprecated hidden by default", passesFilters({ status: "deprecated"
821821
eq("engine: url round-trips", readState("?engine=unity", URL_DEF, { engine: ["any", ...Object.keys(EN)] }).engine, "unity");
822822
eq("engine: written to the url", stateQuery({ ...URL_DEF, engine: "godot" }, URL_DEF), "engine=godot");
823823
eq("engine: unknown value ignored", readState("?engine=cryengine", URL_DEF, { engine: ["any", ...Object.keys(EN)] }).engine, "any");
824+
const { nativeEngines: ne, worksInEngine: wie, placementKey: pk } = await import("./search.mjs");
825+
const on = (formats, engine) => passesFilters({ status: "active", formats }, { ...URL_DEF, engine });
826+
eq("engine: godot hides an Unreal project", on(["Unreal"], "godot"), false);
827+
eq("engine: godot hides an Unreal plugin with its C++", on(["unreal-plugin", "C++"], "godot"), false);
828+
eq("engine: unity hides a Godot addon", on(["godot-addon"], "unity"), false);
829+
eq("engine: unity hides a Godot project", on(["gdshader", "Godot"], "unity"), false);
830+
eq("engine: unreal hides the Godot editor", on(["desktop-app", "Godot"], "unreal"), false);
831+
eq("engine: godot hides a Unity-only entry", on(["Unity"], "godot"), false);
832+
eq("engine: an addon stays native despite asset formats", ne({ formats: ["godot-addon", "gdextension", "EXR", "PNG"] }).join(), "godot");
833+
eq("engine: a sound pack with a Godot demo is agnostic", on(["WAV", "Godot"], "unity"), true);
834+
eq("engine: a multi-engine export is agnostic", ne({ formats: ["Unity", "Godot", "JSON", "PNG"] }).length, 0);
835+
eq("engine: a pack for two engines stays under either", on(["Unity", "Unreal"], "unity"), true);
836+
eq("engine: godot keeps Godot addons", on(["godot-addon"], "godot"), true);
837+
eq("engine: no formats is agnostic", wie({}, "godot"), true);
838+
eq("engine: an unknown engine hides nothing", wie({ formats: ["Unreal"] }, "pico-8"), true);
839+
eq("engine: native format ranks first", er({ formats: ["unreal-plugin"] }, "unreal"), 0);
840+
eq("placement: key changes with the engine", pk({ sort: "name", engine: "godot" }, { ranked: false, group: true, list: [] }) === pk({ sort: "name", engine: "unity" }, { ranked: false, group: true, list: [] }), false);
841+
eq("placement: key ignores filters under a plain sort", pk({ sort: "name", engine: "any", q: "x" }, { ranked: false, group: false, list: [{ id: "a" }] }), pk({ sort: "name" }, { ranked: false, group: false, list: [] }));
842+
eq("placement: a ranked key follows the list", pk({ sort: "name" }, { ranked: true, group: false, list: [{ id: "a" }, { id: "b" }] }), "rank:false:a,b");
824843
}
825844
eq("filter: commercial-only keeps varies", passesFilters({ status: "active", commercial: "varies" }, { ...URL_DEF, commercialOnly: true }), true);
826845
eq("filter: commercial-only drops unknown", passesFilters({ status: "active", commercial: "unknown" }, { ...URL_DEF, commercialOnly: true }), false);
@@ -1337,6 +1356,7 @@ eq("search: q from the URL is capped", readState(`?q=${"a".repeat(5000)}`, { q:
13371356
eq("order: engine=godot puts godot-tagged entries first", ids([...E].sort(sorterFor({ sort: "verified", engine: "godot" }))), "acdb");
13381357
eq("order: engine=unreal puts engine-locked entries first", ids([...E].sort(sorterFor({ sort: "name", engine: "unreal" }))), "dabc");
13391358
eq("order: engineRank with an unknown engine is 0", engineRank(E[0], "pico-8"), 0);
1359+
eq("order: a ranked search breaks score ties by engine", ids(run({ engine: "unreal" }, { score: (e) => (e.id === "b" ? 0 : 1), hasQuery: true }).list), "dac");
13401360
eq("order: SORTS.name ties nothing", SORTS.name(E[0], E[0]), 0);
13411361

13421362
const filtered = run({ category: "audio" }, { passes: (e) => e.id !== "d" });

‎site/lib/search.mjs‎

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function passesFilters(entry, state, formatGroups = {}) {
7878
if (state.noAttr && entry.attribution_required !== false) return false;
7979
if (state.perspective !== "any" && entry.camera_perspective !== state.perspective) return false;
8080
if (state.license !== "any" && entry.licenseFamily !== state.license) return false;
81-
if (state.engine && state.engine !== "any" && entry.engine_lock && entry.engine_lock !== state.engine) return false;
81+
if (state.engine && state.engine !== "any" && !worksInEngine(entry, state.engine)) return false;
8282
if (state.format !== "any") {
8383
const wanted = formatGroups[state.format]?.formats || [];
8484
if (!(entry.formats || []).some((f) => wanted.includes(f))) return false;
@@ -88,21 +88,57 @@ export function passesFilters(entry, state, formatGroups = {}) {
8888

8989
/** Choice params: URL key -> state field. `allowed` supplies each field's legal values. */
9090
/**
91-
* The engine filter. An entry locked to another engine (engine_lock) is
92-
* hidden; entries tagged for the chosen engine sort first. Everything else
93-
* is engine-agnostic and stays listed.
91+
* The engine filter. An entry is hidden when it only works in another
92+
* engine: its licence is locked to one (engine_lock), or everything it
93+
* ships is native to one (nativeEngines). Entries made for the chosen
94+
* engine sort first. Everything else is engine-agnostic and stays listed.
95+
*
96+
* `formats` are the engine-native formats: a project, addon, plugin or
97+
* shader that only that engine opens.
9498
*/
9599
export const ENGINES = {
96-
godot: { label: "Godot", tags: ["godot", "godot-4"] },
97-
unity: { label: "Unity", tags: ["unity"] },
98-
unreal: { label: "Unreal Engine", tags: ["unreal-engine", "unreal"] },
100+
godot: { label: "Godot", tags: ["godot", "godot-4"], formats: ["Godot", "godot-addon", "gdextension", "gdshader"] },
101+
unity: { label: "Unity", tags: ["unity"], formats: ["Unity"] },
102+
unreal: { label: "Unreal Engine", tags: ["unreal-engine", "unreal"], formats: ["Unreal", "unreal-plugin"] },
99103
};
100104

101-
/** 0 when an entry is made for `engine` (tagged for it or locked to it), else 1. */
105+
/**
106+
* Formats that ride along with an engine-native one without making an entry
107+
* portable: an Unreal plugin's C++ source, the Godot editor's desktop app.
108+
*/
109+
const ENGINE_COMPANION_FORMATS = new Set(["C++", "desktop-app"]);
110+
111+
/**
112+
* The engines an entry is native to, or [] when it is engine-agnostic.
113+
* Plugins and addons (godot-addon, gdextension, unreal-plugin) make an
114+
* entry native whatever else it lists, since the files only load in that
115+
* engine. Otherwise an entry is native only when every format it lists
116+
* belongs to an engine: [WAV, Godot] is a sound pack with a Godot demo,
117+
* usable anywhere, while [gdshader, Godot] is a Godot project.
118+
*/
119+
export function nativeEngines(entry) {
120+
const formats = entry.formats || [];
121+
const engines = Object.keys(ENGINES).filter((k) => formats.some((f) => ENGINES[k].formats.includes(f)));
122+
if (!engines.length) return [];
123+
if (formats.some((f) => ["godot-addon", "gdextension", "unreal-plugin"].includes(f))) return engines;
124+
const portable = formats.some((f) => !ENGINE_COMPANION_FORMATS.has(f) && !Object.values(ENGINES).some((e) => e.formats.includes(f)));
125+
return portable ? [] : engines;
126+
}
127+
128+
/** False when the entry only works in an engine other than `engine`: by licence (engine_lock) or by format. */
129+
export function worksInEngine(entry, engine) {
130+
if (!ENGINES[engine]) return true;
131+
if (entry.engine_lock && entry.engine_lock !== engine) return false;
132+
const native = nativeEngines(entry);
133+
return !native.length || native.includes(engine);
134+
}
135+
136+
/** 0 when an entry is made for `engine` (tagged for it, native to it or locked to it), else 1. */
102137
export function engineRank(entry, engine) {
103138
const e = ENGINES[engine];
104139
if (!e) return 0;
105140
if (entry.engine_lock === engine) return 0;
141+
if ((entry.formats || []).some((f) => e.formats.includes(f))) return 0;
106142
return (entry.tags || []).some((t) => e.tags.includes(t)) ? 0 : 1;
107143
}
108144

@@ -161,11 +197,25 @@ export function computeResults(entries, state, { passes, score, hasQuery }) {
161197
for (const e of pool) counts.set(e.category, (counts.get(e.category) || 0) + 1);
162198
const list = state.category === "all" ? pool.slice() : pool.filter((e) => e.category === state.category);
163199
const ranked = state.sort === "name" && hasQuery;
164-
if (ranked) list.sort((a, b) => scores.get(b.id) - scores.get(a.id) || SORTS.name(a, b));
165-
else list.sort(sorterFor(state));
200+
if (ranked) {
201+
// Equal scores fall back to the name sort, engine picks first.
202+
const tie = sorterFor({ ...state, sort: "name" });
203+
list.sort((a, b) => scores.get(b.id) - scores.get(a.id) || tie(a, b));
204+
} else list.sort(sorterFor(state));
166205
return { list, counts, total: pool.length, ranked };
167206
}
168207

208+
/**
209+
* When this key changes, app.js re-places every card. Under a plain sort
210+
* the order of all cards depends only on the grouping, the sort and the
211+
* engine (filters just toggle visibility); a ranked search reorders on every
212+
* new result list.
213+
*/
214+
export function placementKey(state, { ranked, group, list }) {
215+
if (ranked) return `rank:${group}:${list.map((e) => e.id).join(",")}`;
216+
return `${group ? "group" : "flat"}:${state.sort}:${state.engine || "any"}`;
217+
}
218+
169219
const CHOICES = [
170220
["cat", "category"],
171221
["sort", "sort"],

‎site/public/app.js‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { closestEntries, computeResults as computeResultsFor, passesFilters as passes, queryTests as compileQuery, readState, scoreFields, searchFields, shouldGroup as shouldGroupFor, slugOf, sorterFor, SORTS, stateQuery, ENGINES } from "./search.js";
1+
import { closestEntries, computeResults as computeResultsFor, passesFilters as passes, placementKey, queryTests as compileQuery, readState, scoreFields, searchFields, shouldGroup as shouldGroupFor, slugOf, sorterFor, SORTS, stateQuery, ENGINES } from "./search.js";
22
import { CARD_LIMIT, LIST_LIMIT, commercialLabel, entryCardHtml, escapeHtml, groupHeadingHtml, groupMoreHtml, PERSPECTIVE_LABELS } from "./cards.mjs";
33
import { installViewToggles } from "./views.js";
44

@@ -415,11 +415,10 @@ import { installViewToggles } from "./views.js";
415415
if (!preview) ensureAllNodes();
416416
else ensurePreviewNodes(list);
417417

418-
// Under a plain sort the order of all nodes is fixed, so filtering only
419-
// toggles visibility. A ranked search reorders on every change.
420-
const key = ranked
421-
? `rank:${group}:${list.map((e) => e.id).join(",")}`
422-
: `${group ? "group" : "flat"}:${state.sort}`;
418+
// Under a plain sort the order of all nodes depends only on the sort
419+
// and the engine, so filtering only toggles visibility. A ranked search
420+
// reorders on every change (search.mjs placementKey).
421+
const key = placementKey(state, { ranked, group, list });
423422
if (key !== placedKey) {
424423
placedKey = key;
425424
const shown = new Set(list.map((e) => e.id));

‎site/public/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ <h2 id="catalog-title">Catalog</h2>
140140
<option value="any">Any</option>
141141
</select>
142142
</label>
143-
<label class="check sort-field" title="Hides entries licensed for another engine only, and lists ones made for this engine first">
143+
<label class="check sort-field" title="Hides projects, addons and plugins for another engine and assets licensed for one engine only, and lists ones made for this engine first">
144144
<span>Engine</span>
145145
<select id="engine">
146146
<option value="any">Any</option>

0 commit comments

Comments
 (0)