Implement new Avro schema for creatures (#211)
The creature state stored in #210, while quite compact, is also very fragile, because any minor change in our creature generation algorithm or even vocabulary will completely throw off existing serializations. This attempts to serialize the actual _creature_ created by the generator rather than its random number seed, which will be a larger serialization but also one that is more resilient to future changes in the algorithm and/or vocabulary. It should also pave the way for tweaking generated creatures. Note that the previous `v1` schema implemented in #210 is no longer supported--i.e., links that use it will now be broken. Hopefully this is OK because it hasn't been around for very long, but it was just way too much work to migrate that schema to the new `v2` format.pull/212/head
rodzic
25bff780a7
commit
beb12345a1
|
@ -34,6 +34,7 @@ import { Checkbox } from "../../checkbox";
|
|||
import {
|
||||
CompositionContextWidget,
|
||||
createSvgCompositionContext,
|
||||
SvgCompositionContext,
|
||||
} from "../../svg-composition-context";
|
||||
import { Page } from "../../page";
|
||||
import { RandomizerWidget } from "../../randomizer-widget";
|
||||
|
@ -206,8 +207,8 @@ const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1;
|
|||
|
||||
const INITIAL_COMPLEXITY_LEVEL = 2;
|
||||
|
||||
function getDownloadBasename(randomSeed: number) {
|
||||
return `mystic-symbolic-creature-${randomSeed}`;
|
||||
function getDownloadBasename(rootSymbolName: string) {
|
||||
return `mystic-symbolic-creature-${rootSymbolName}`;
|
||||
}
|
||||
|
||||
function creatureHasSymbol(
|
||||
|
@ -244,55 +245,56 @@ function repeatUntilSymbolIsIncluded(
|
|||
return createCreature(rng);
|
||||
}
|
||||
|
||||
export const CREATURE_DESIGN_DEFAULTS = {
|
||||
randomSeed: 0,
|
||||
randomlyInvert: true,
|
||||
complexity: INITIAL_COMPLEXITY_LEVEL,
|
||||
alwaysIncludeSymbol: EMPTY_SVG_SYMBOL_DATA,
|
||||
compCtx: createSvgCompositionContext(),
|
||||
export type CreatureDesign = {
|
||||
compCtx: SvgCompositionContext;
|
||||
creature: CreatureSymbol;
|
||||
};
|
||||
|
||||
export type CreatureDesign = typeof CREATURE_DESIGN_DEFAULTS;
|
||||
export const CREATURE_DESIGN_DEFAULTS: CreatureDesign = {
|
||||
compCtx: createSvgCompositionContext(),
|
||||
creature: {
|
||||
data: ROOT_SYMBOLS[0],
|
||||
invertColors: false,
|
||||
attachments: [],
|
||||
nests: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const CreaturePageWithDefaults: React.FC<
|
||||
ComponentWithShareableStateProps<CreatureDesign>
|
||||
> = ({ defaults, onChange }) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [randomSeed, setRandomSeed] = useState(defaults.randomSeed);
|
||||
const [randomlyInvert, setRandomlyInvert] = useState(defaults.randomlyInvert);
|
||||
const [randomlyInvert, setRandomlyInvert] = useState(true);
|
||||
const [compCtx, setCompCtx] = useState(defaults.compCtx);
|
||||
const [complexity, setComplexity] = useState(defaults.complexity);
|
||||
const [complexity, setComplexity] = useState(INITIAL_COMPLEXITY_LEVEL);
|
||||
const [creature, setCreature] = useState(defaults.creature);
|
||||
const defaultCtx = useContext(CreatureContext);
|
||||
const newRandomSeed = () => setRandomSeed(Date.now());
|
||||
const ctx: CreatureContextType = noFillIfShowingSpecs({
|
||||
...defaultCtx,
|
||||
...compCtx,
|
||||
});
|
||||
const [alwaysInclude, setAlwaysInclude] = useState<SvgSymbolData>(
|
||||
defaults.alwaysIncludeSymbol
|
||||
);
|
||||
const creature = useMemo(
|
||||
() =>
|
||||
const newRandomCreature = () => {
|
||||
setCreature(
|
||||
repeatUntilSymbolIsIncluded(
|
||||
alwaysInclude,
|
||||
new Random(randomSeed),
|
||||
new Random(Date.now()),
|
||||
(rng) =>
|
||||
COMPLEXITY_LEVEL_GENERATORS[complexity]({
|
||||
rng,
|
||||
randomlyInvert,
|
||||
})
|
||||
),
|
||||
[alwaysInclude, complexity, randomSeed, randomlyInvert]
|
||||
)
|
||||
);
|
||||
};
|
||||
const ctx: CreatureContextType = noFillIfShowingSpecs({
|
||||
...defaultCtx,
|
||||
...compCtx,
|
||||
});
|
||||
const [alwaysInclude, setAlwaysInclude] = useState<SvgSymbolData>(
|
||||
EMPTY_SVG_SYMBOL_DATA
|
||||
);
|
||||
const design: CreatureDesign = useMemo(
|
||||
() => ({
|
||||
randomSeed,
|
||||
randomlyInvert,
|
||||
complexity,
|
||||
alwaysIncludeSymbol: alwaysInclude,
|
||||
creature,
|
||||
compCtx,
|
||||
}),
|
||||
[randomSeed, randomlyInvert, complexity, alwaysInclude, compCtx]
|
||||
[creature, compCtx]
|
||||
);
|
||||
|
||||
useDebouncedEffect(
|
||||
|
@ -313,7 +315,7 @@ export const CreaturePageWithDefaults: React.FC<
|
|||
value={complexity}
|
||||
onChange={(value) => {
|
||||
setComplexity(value);
|
||||
newRandomSeed();
|
||||
newRandomCreature();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -326,7 +328,7 @@ export const CreaturePageWithDefaults: React.FC<
|
|||
</div>
|
||||
<RandomizerWidget
|
||||
onColorsChange={(colors) => setCompCtx({ ...compCtx, ...colors })}
|
||||
onSymbolsChange={newRandomSeed}
|
||||
onSymbolsChange={newRandomCreature}
|
||||
>
|
||||
<div className="thingy">
|
||||
<VocabularyWidget
|
||||
|
@ -339,7 +341,7 @@ export const CreaturePageWithDefaults: React.FC<
|
|||
</RandomizerWidget>
|
||||
<div className="thingy">
|
||||
<ExportWidget
|
||||
basename={getDownloadBasename(randomSeed)}
|
||||
basename={getDownloadBasename(creature.data.name)}
|
||||
svgRef={svgRef}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
"type": "record",
|
||||
"name": "AvroCreatureDesign",
|
||||
"fields": [
|
||||
{ "name": "randomSeed", "type": "long" },
|
||||
{ "name": "randomlyInvert", "type": "boolean" },
|
||||
{ "name": "complexity", "type": "int" },
|
||||
{ "name": "alwaysIncludeSymbol", "type": "string" },
|
||||
{
|
||||
"name": "compCtx",
|
||||
"type": {
|
||||
|
@ -19,6 +15,46 @@
|
|||
{ "name": "disableGradients", "type": "boolean", "default": true }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "creature",
|
||||
"type": {
|
||||
"name": "AvroCreatureSymbol",
|
||||
"type": "record",
|
||||
"fields": [
|
||||
{ "name": "symbol", "type": "string" },
|
||||
{ "name": "invertColors", "type": "boolean" },
|
||||
{
|
||||
"name": "attachments",
|
||||
"type": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"name": "AvroAttachedCreatureSymbol",
|
||||
"type": "record",
|
||||
"fields": [
|
||||
{ "name": "base", "type": "AvroCreatureSymbol" },
|
||||
{ "name": "attachTo", "type": "int" },
|
||||
{ "name": "indices", "type": "bytes" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nests",
|
||||
"type": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"name": "AvroNestedCreatureSymbol",
|
||||
"type": "record",
|
||||
"fields": [
|
||||
{ "name": "base", "type": "AvroCreatureSymbol" },
|
||||
{ "name": "indices", "type": "bytes" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,29 +1,107 @@
|
|||
import * as avro from "avro-js";
|
||||
import { CreatureDesign } from "./core";
|
||||
import { AvroCreatureDesign } from "./creature-design.avsc";
|
||||
import {
|
||||
AvroAttachedCreatureSymbol,
|
||||
AvroCreatureDesign,
|
||||
AvroCreatureSymbol,
|
||||
AvroNestedCreatureSymbol,
|
||||
} from "./creature-design.avsc";
|
||||
import { fromBase64, toBase64 } from "../../base64";
|
||||
import CreatureAvsc from "./creature-design.avsc.json";
|
||||
import { SvgVocabularyWithBlank } from "../../svg-vocabulary";
|
||||
import { Packer, SvgCompositionContextPacker } from "../../serialization";
|
||||
import {
|
||||
AttachedCreatureSymbol,
|
||||
CreatureSymbol,
|
||||
NestedCreatureSymbol,
|
||||
} from "../../creature-symbol";
|
||||
import { SvgVocabulary } from "../../svg-vocabulary";
|
||||
import { ATTACHMENT_POINT_TYPES } from "../../specs";
|
||||
|
||||
const LATEST_VERSION = "v1";
|
||||
const LATEST_VERSION = "v2";
|
||||
|
||||
const avroCreatureDesign = avro.parse<AvroCreatureDesign>(CreatureAvsc);
|
||||
|
||||
const DesignConfigPacker: Packer<CreatureDesign, AvroCreatureDesign> = {
|
||||
const ATTACHMENT_POINT_MAPPING = new Map(
|
||||
ATTACHMENT_POINT_TYPES.map((name, i) => {
|
||||
return [name, i];
|
||||
})
|
||||
);
|
||||
|
||||
const NestedCreatureSymbolPacker: Packer<
|
||||
NestedCreatureSymbol,
|
||||
AvroNestedCreatureSymbol
|
||||
> = {
|
||||
pack: (value) => {
|
||||
return {
|
||||
base: CreatureSymbolPacker.pack(value),
|
||||
indices: Buffer.from(value.indices),
|
||||
};
|
||||
},
|
||||
unpack: (value) => {
|
||||
return {
|
||||
...CreatureSymbolPacker.unpack(value.base),
|
||||
indices: Array.from(value.indices),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const AttachedCreatureSymbolPacker: Packer<
|
||||
AttachedCreatureSymbol,
|
||||
AvroAttachedCreatureSymbol
|
||||
> = {
|
||||
pack: (value) => {
|
||||
const attachTo = ATTACHMENT_POINT_MAPPING.get(value.attachTo);
|
||||
if (attachTo === undefined) {
|
||||
throw new Error(`Invalid attachment type "${value.attachTo}"`);
|
||||
}
|
||||
return {
|
||||
base: CreatureSymbolPacker.pack(value),
|
||||
attachTo,
|
||||
indices: Buffer.from(value.indices),
|
||||
};
|
||||
},
|
||||
unpack: (value) => {
|
||||
const attachTo = ATTACHMENT_POINT_TYPES[value.attachTo];
|
||||
if (attachTo === undefined) {
|
||||
throw new Error(`Invalid attachment type "${value.attachTo}"`);
|
||||
}
|
||||
return {
|
||||
...CreatureSymbolPacker.unpack(value.base),
|
||||
attachTo,
|
||||
indices: Array.from(value.indices),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const CreatureSymbolPacker: Packer<CreatureSymbol, AvroCreatureSymbol> = {
|
||||
pack: (value) => {
|
||||
return {
|
||||
...value,
|
||||
alwaysIncludeSymbol: value.alwaysIncludeSymbol.name,
|
||||
compCtx: SvgCompositionContextPacker.pack(value.compCtx),
|
||||
symbol: value.data.name,
|
||||
attachments: value.attachments.map(AttachedCreatureSymbolPacker.pack),
|
||||
nests: value.nests.map(NestedCreatureSymbolPacker.pack),
|
||||
};
|
||||
},
|
||||
unpack: (value) => {
|
||||
return {
|
||||
...value,
|
||||
alwaysIncludeSymbol: SvgVocabularyWithBlank.get(
|
||||
value.alwaysIncludeSymbol
|
||||
),
|
||||
data: SvgVocabulary.get(value.symbol),
|
||||
attachments: value.attachments.map(AttachedCreatureSymbolPacker.unpack),
|
||||
nests: value.nests.map(NestedCreatureSymbolPacker.unpack),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const DesignConfigPacker: Packer<CreatureDesign, AvroCreatureDesign> = {
|
||||
pack: (value) => {
|
||||
return {
|
||||
creature: CreatureSymbolPacker.pack(value.creature),
|
||||
compCtx: SvgCompositionContextPacker.pack(value.compCtx),
|
||||
};
|
||||
},
|
||||
unpack: (value) => {
|
||||
return {
|
||||
creature: CreatureSymbolPacker.unpack(value.creature),
|
||||
compCtx: SvgCompositionContextPacker.unpack(value.compCtx),
|
||||
};
|
||||
},
|
||||
|
@ -35,7 +113,10 @@ export function serializeCreatureDesign(value: CreatureDesign): string {
|
|||
}
|
||||
|
||||
export function deserializeCreatureDesign(value: string): CreatureDesign {
|
||||
const [_version, serialized] = value.split(".", 2);
|
||||
const [version, serialized] = value.split(".", 2);
|
||||
if (version === "v1") {
|
||||
throw new Error(`Sorry, we no longer support loading v1 creatures!`);
|
||||
}
|
||||
const buf = fromBase64(serialized);
|
||||
return DesignConfigPacker.unpack(avroCreatureDesign.fromBuffer(buf));
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue