Skip to content

Commit

Permalink
add dna string im/exporting
Browse files Browse the repository at this point in the history
  • Loading branch information
diogotr7 committed May 11, 2024
1 parent 0647352 commit 577e2c7
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 119 deletions.
89 changes: 45 additions & 44 deletions src/Chf/Dna.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export type DnaFacePart =
'crown'

export interface DnaBlend {
part: DnaFacePart
headId: number
percent: number
}
Expand All @@ -61,17 +60,55 @@ export interface Dna {
blends: DnaBlends
}

export function readDna(parentReader: BufferReader, bodyType: BodyType): Dna {
const isMale = bodyType === 'male'
export function readDna(parentReader: BufferReader): Dna {
parentReader.expectUint64(dnaSize)

const bytes = parentReader.readBytes(dnaSize)
const dnaString = toHexStr(bytes)
const reader = new BufferReader(bytes.buffer)

return dnaFromString(dnaString)
}

export function writeDna(writer: BufferWriter, dna: Dna, _bodyType: BodyType) {
const bytes = fromHexStr(dna.dnaString)
if (bytes.length !== dnaSize)
throw new Error(`DNA string must be ${dnaSize} bytes long`)

writer.writeUint64(dnaSize)
writer.writeBytes(bytes)

// TODO: this is probably broken. Replace the above with this when fixed
// const isMale = bodyType === 'male'
// writer.writeUint32(0xFCD09394)
// writer.writeUint32(isMale ? 0xDD6C67F6 : 0x9EF4EB54)
// writer.writeUint32(isMale ? 0x65E740D3 : 0x65D75204)
// writer.writeUint32(0)
// writer.writeByte(0x0C)
// writer.writeByte(0x0)
// writer.writeByte(0x04)
// writer.writeByte(0x0)
// writer.writeByte(0x4)
// writer.writeByte(0x0)
// writer.writeByte(dna.childCount)
// writer.writeByte(0x0)

// for (let i = 0; i < partCount; i++) {
// const part = i % 12
// const blends = dna.blends.get(part) || []
// const blend = blends[i % blends.length]

// const percentShort = blend.percent / 100 * 0xFFFF
// writer.writeUint16(percentShort)
// writer.writeByte(blend.headId)
// writer.writeByte(0)
// }
}

export function dnaFromString(dnaString: string): Dna {
const reader = new BufferReader(fromHexStr(dnaString).buffer)
reader.expectUint32(0xFCD09394)
reader.expectUint32(isMale ? 0xDD6C67F6 : 0x9EF4EB54)
reader.expectUint32(isMale ? 0x65E740D3 : 0x65D75204)
reader.readUint32()// skip keys. bad idea?
reader.readUint32()
reader.expectUint32(0)
reader.expectByte(0x0C)
reader.expectByte(0x0)
Expand All @@ -80,7 +117,7 @@ export function readDna(parentReader: BufferReader, bodyType: BodyType): Dna {
reader.expectByte(0x4)
reader.expectByte(0x0)
const childCount = reader.readByte()
reader.expectByte(0x0)
reader.readByte()// might be 0 or ff for some reason?

const map: DnaBlends = {
eyebrowLeft: [],
Expand All @@ -104,9 +141,8 @@ export function readDna(parentReader: BufferReader, bodyType: BodyType): Dna {
reader.expectByte(0)

const percent = percentShort / 0xFFFF * 100
const blend = { headId, percent, part: idxPartRecord[part] }

map[idxPartRecord[part]].push(blend)
map[idxPartRecord[part]].push({ headId, percent })
}

return {
Expand All @@ -115,38 +151,3 @@ export function readDna(parentReader: BufferReader, bodyType: BodyType): Dna {
blends: map,
}
}

export function writeDna(writer: BufferWriter, dna: Dna, _bodyType: BodyType) {
const bytes = fromHexStr(dna.dnaString)
if (bytes.length !== dnaSize)
throw new Error(`DNA string must be ${dnaSize} bytes long`)

writer.writeUint64(dnaSize)
writer.writeBytes(bytes)

// TODO: this is probably broken. Replace the above with this when fixed
// const isMale = bodyType === 'male'
// writer.writeUint32(0xFCD09394)
// writer.writeUint32(isMale ? 0xDD6C67F6 : 0x9EF4EB54)
// writer.writeUint32(isMale ? 0x65E740D3 : 0x65D75204)
// writer.writeUint32(0)
// writer.writeByte(0x0C)
// writer.writeByte(0x0)
// writer.writeByte(0x04)
// writer.writeByte(0x0)
// writer.writeByte(0x4)
// writer.writeByte(0x0)
// writer.writeByte(dna.childCount)
// writer.writeByte(0x0)

// for (let i = 0; i < partCount; i++) {
// const part = i % 12
// const blends = dna.blends.get(part) || []
// const blend = blends[i % blends.length]

// const percentShort = blend.percent / 100 * 0xFFFF
// writer.writeUint16(percentShort)
// writer.writeByte(blend.headId)
// writer.writeByte(0)
// }
}
14 changes: 7 additions & 7 deletions src/Components/CharacterEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Affix, Button, Center, Group, Stack } from '@mantine/core'
import { IconDownload } from '@tabler/icons-react'
import { useCallback } from 'react'
import { useLocalStorage } from '@mantine/hooks'
import { useCharacter } from '../Context/CharacterContext'
import { createChf } from '../Chf/ChfFile'
import SkinColorPicker from './SkinColorPicker'
import { CharacterJsonDisplay } from './CharacterJsonDisplay'
import { DnaPanel } from './DnaPanel'

function CharacterEditor() {
const isDev = import.meta.env.DEV
const [isDev] = useLocalStorage({
key: 'isDev',
defaultValue: false,
})
const [character] = useCharacter()
const exportCharacter = useCallback(() => {
const buffer = createChf(character)
Expand All @@ -25,13 +29,9 @@ function CharacterEditor() {
<Stack justify="flex-start">
<Group justify="space-evenly">
<SkinColorPicker />

<DnaPanel />

{isDev && (
<CharacterJsonDisplay />
)}
{isDev && <DnaPanel />}
</Group>
{isDev && <CharacterJsonDisplay />}
</Stack>
<Affix zIndex={900} position={{ bottom: 0, left: 0, right: 0 }}>
<Center p="xl">
Expand Down
191 changes: 123 additions & 68 deletions src/Components/DnaPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,152 @@
import { Fieldset, Group, NumberInput, Slider, Stack } from '@mantine/core'
import { useCallback } from 'react'
import { Button, Center, Fieldset, Group, Input, Modal, NumberInput, Slider, Stack, Text } from '@mantine/core'
import { useCallback, useState } from 'react'
import type { NumberFormatValues } from 'react-number-format'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { useCharacter } from '../Context/CharacterContext'
import type { DnaFacePart } from '../Chf/Dna'
import { type DnaFacePart, dnaFromString } from '../Chf/Dna'

export function DnaPanel() {
const [opened, { toggle, close }] = useDisclosure(false)
const [dnaString, setDnaString] = useState('')
const [character, updateCharacter] = useCharacter()

const importDna = useCallback(() => {
updateCharacter((d) => {
d.dna = dnaFromString(dnaString)
})
close()
}, [updateCharacter, close, dnaString])

const dnaStringOpen = useCallback(() => {
setDnaString('')
toggle()
}, [toggle, setDnaString])

const dnaStringClipboard = useCallback(() => {
navigator.clipboard.writeText(character.dna.dnaString)
notifications.show({
title: 'DNA String copied to clipboard',
message: 'You can now share it with others or import it into another character.',
autoClose: 2000,
})
}, [character])

return (
<Fieldset legend="DNA">
<Stack>
<Group>
<DnaPart label="Left Eyebrow" part="eyebrowLeft" />
<DnaPart label="Right Eyebrow" part="eyebrowRight" />
<DnaPart label="Left Eye" part="eyeLeft" />
<DnaPart label="Right Eye" part="eyeRight" />
</Group>
<Group>
<DnaPart label="Left Ear" part="earLeft" />
<DnaPart label="Right Ear" part="earRight" />
<DnaPart label="Left Cheek" part="cheekLeft" />
<DnaPart label="Right Cheek" part="cheekRight" />
</Group>
<Group>
<DnaPart label="Nose" part="nose" />
<DnaPart label="Mouth" part="mouth" />
<DnaPart label="Jaw" part="jaw" />
<DnaPart label="Crown" part="crown" />
</Group>
</Stack>
</Fieldset>
<>
<Fieldset legend="DNA">
<Stack>
<Group>
<DnaPart label="Left Eyebrow" part="eyebrowLeft" />
<DnaPart label="Right Eyebrow" part="eyebrowRight" />
<DnaPart label="Left Eye" part="eyeLeft" />
<DnaPart label="Right Eye" part="eyeRight" />
</Group>
<Group>
<DnaPart label="Left Ear" part="earLeft" />
<DnaPart label="Right Ear" part="earRight" />
<DnaPart label="Left Cheek" part="cheekLeft" />
<DnaPart label="Right Cheek" part="cheekRight" />
</Group>
<Group>
<DnaPart label="Nose" part="nose" />
<DnaPart label="Mouth" part="mouth" />
<DnaPart label="Jaw" part="jaw" />
<DnaPart label="Crown" part="crown" />
</Group>
<Group justify="center">
<Button onClick={dnaStringOpen}>
Import DNA String
</Button>
<Button onClick={dnaStringClipboard}>
Export to Clipboard
</Button>
</Group>
</Stack>
</Fieldset>
<Modal
// export button z-index is 900
zIndex={1000}
title="Import DNA String"
opened={opened}
size="xl"
onClose={close}
closeOnClickOutside={false}
closeOnEscape={false}
overlayProps={{
backgroundOpacity: 0.55,
blur: 3,
}}
centered
>
<Stack>
<Text size="lg">
Enter the DNA string of the character you want to import. These can be shared between players or imported from NPCs from the game files.
</Text>
<Input
value={dnaString}
onChange={event => setDnaString(event.currentTarget.value)}
size="xl"
w="100%"
placeholder="9493D0FC...."
/>
<Center pt="md">
<Button onClick={importDna}>
Import
</Button>
</Center>
</Stack>
</Modal>
</>
)
}

function DnaPart({ label, part }: { label: string, part: DnaFacePart }) {
const [character] = useCharacter()
const [character, updateCharacter] = useCharacter()
const blend = character.dna.blends[part]

// make this work properly. We want the sum of all the blend percentages to be 100
// and the others to be updated accordingly when one is changed
// const updateBlend = useCallback((sliderIndex: number, newValue: number) => {

// }, [blend, character, part, updateCharacter])
const updateBlend = useCallback((sliderIndex: number, newValue: number) => {
console.log('updateBlend', sliderIndex, newValue)
}, [blend, character, part, updateCharacter])

const isValidHeadId = useCallback((headId: NumberFormatValues) => {
const id = headId.floatValue ?? 0
// double check this
return id >= 0 && id <= 26 && Number.isInteger(id)
}, [])

function HeadPicker({ index }: { index: number }) {
return (
<NumberInput
allowDecimal={false}
allowLeadingZeros={false}
allowNegative={false}
isAllowed={isValidHeadId}
size="xs"
w={50}
value={blend[index].headId}
disabled
// onValueChange={values => updateCharacter((d) => { d.dna.blends[part][index].headId = values.floatValue ?? 0 })}
/>
)
}

function MySlider({ index }: { index: number }) {
function DnaBlend({ index }: { index: number }) {
return (
<Slider
w="100"
disabled
value={blend[index].percent}
// onChange={value => updateBlend(index, value)}
/>
<Group>
<NumberInput
allowDecimal={false}
allowLeadingZeros={false}
allowNegative={false}
isAllowed={isValidHeadId}
size="xs"
w={50}
value={blend[index].headId}
disabled
onValueChange={values => updateCharacter((d) => { d.dna.blends[part][index].headId = values.floatValue ?? 0 })}
/>
<Slider
w="100"
disabled
value={blend[index].percent}
onChange={value => updateBlend(index, value)}
/>
</Group>
)
}

return (
<Fieldset legend={label}>
<Group>
<HeadPicker index={0} />
<MySlider index={0} />
</Group>
<Group>
<HeadPicker index={1} />
<MySlider index={1} />
</Group>
<Group>
<HeadPicker index={2} />
<MySlider index={2} />
</Group>
<Group>
<HeadPicker index={3} />
<MySlider index={3} />
</Group>
<DnaBlend index={0} />
<DnaBlend index={1} />
<DnaBlend index={2} />
<DnaBlend index={3} />
</Fieldset>
)
}

0 comments on commit 577e2c7

Please sign in to comment.