Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
package external
// Clipboard represents a temporary storage.
type Clipboard interface {
// String returns the current value of the clipboard, if it is compatible with UTF-8.
String() (string, error)
// SetString sets the current value of the clipboard as UTF-8 string.
SetString(value string)
}
+71
View File
@@ -0,0 +1,71 @@
package external
import (
"image"
"image/png"
"os"
"path/filepath"
"github.com/inkyblackness/hacked/ss1/content/audio"
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ui/gui"
)
// Export starts an export dialog series, calling the given callback with a folder name.
func Export(machine gui.ModalStateMachine, info string, callback func(string), lastFailed bool) {
machine.SetState(&exportStartState{
machine: machine,
callback: callback,
info: info,
withError: lastFailed,
})
}
// ExportAudio is a helper wrapper for exporting audio.
func ExportAudio(machine gui.ModalStateMachine, filename string, sound audio.L8) {
info := "File to be written: " + filename
var dirHandler func(string)
dirHandler = func(dirname string) {
writer, err := os.Create(filepath.Join(dirname, filename))
if err != nil {
Export(machine, info, dirHandler, true)
return
}
defer func() { _ = writer.Close() }()
err = wav.Save(writer, sound.SampleRate, sound.Samples)
if err != nil {
Export(machine, info, dirHandler, true)
}
}
Export(machine, info, dirHandler, false)
}
// ExportImage is a helper wrapper for exporting single images.
func ExportImage(machine gui.ModalStateMachine, filename string, bmp bitmap.Bitmap) {
info := "File to be written: " + filename
var exportTo func(string)
exportTo = func(dirname string) {
writer, err := os.Create(filepath.Join(dirname, filename))
if err != nil {
Export(machine, "Could not create file.\n"+info, exportTo, true)
return
}
defer func() { _ = writer.Close() }()
imageRect := image.Rect(0, 0, int(bmp.Header.Width), int(bmp.Header.Height))
imagePal := bmp.Palette.ColorPalette(false)
paletted := image.NewPaletted(imageRect, imagePal)
paletted.Pix = bmp.Pixels
err = png.Encode(writer, paletted)
if err != nil {
Export(machine, info, exportTo, true)
return
}
}
Export(machine, info, exportTo, false)
}
+32
View File
@@ -0,0 +1,32 @@
package external
import (
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ui/gui"
)
type exportStartState struct {
machine gui.ModalStateMachine
callback func(string)
info string
withError bool
}
func (state exportStartState) Render() {
imgui.OpenPopup("Export")
nextState := &exportWaitingState{
machine: state.machine,
callback: state.callback,
info: state.info,
}
if state.withError {
nextState.failureTime = time.Now()
}
state.machine.SetState(nextState)
}
func (state exportStartState) HandleFiles(names []string) {
}
+81
View File
@@ -0,0 +1,81 @@
package external
import (
"os"
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/sqweek/dialog"
"github.com/inkyblackness/hacked/ui/gui"
)
type exportWaitingState struct {
machine gui.ModalStateMachine
callback func(string)
info string
failureTime time.Time
}
func (state *exportWaitingState) Render() {
if imgui.BeginPopupModalV("Export", nil,
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
imgui.Text("Waiting for folder.")
if !state.failureTime.IsZero() {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("Previous attempt failed, could not save file.\nPlease check and try again.")
imgui.PopStyleColor()
if time.Since(state.failureTime).Seconds() > 5 {
state.failureTime = time.Time{}
}
}
imgui.Text(`From your file browser drag'n'drop the folder
where to save the data into the editor window.
`)
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("Saving will potentially overwrite existing files in the folder.")
imgui.PopStyleColor()
imgui.Text(state.info)
imgui.Separator()
if imgui.Button("Browse...") {
dlgBuilder := dialog.Directory()
filename, err := dlgBuilder.Browse()
if err == nil {
state.HandleFiles([]string{filename})
}
}
imgui.SameLine()
if imgui.Button("Cancel") {
state.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
imgui.EndPopup()
} else {
state.machine.SetState(nil)
}
}
func (state *exportWaitingState) HandleFiles(names []string) {
dirPath, ok := state.verifyDir(names)
if ok {
state.machine.SetState(nil)
state.callback(dirPath)
} else {
state.failureTime = time.Now()
}
}
func (state exportWaitingState) verifyDir(names []string) (string, bool) {
if len(names) != 1 {
return "", false
}
fileInfo, err := os.Stat(names[0])
if err != nil {
return "", false
}
if !fileInfo.IsDir() {
return "", false
}
return names[0], true
}
+137
View File
@@ -0,0 +1,137 @@
package external
import (
"github.com/inkyblackness/imgui-go/v3"
"github.com/sqweek/dialog"
"github.com/inkyblackness/hacked/ui/gui"
)
// SingleFileHandler is a callback for a single selected file.
type SingleFileHandler func(string) error
type singleFileRunner func(*dialog.FileBuilder) (string, error)
// SaveFile starts a dialog series to save a file.
func SaveFile(machine gui.ModalStateMachine, types []TypeInfo, callback SingleFileHandler) {
machine.SetState(&fileStartState{
machine: machine,
title: "Save",
typeInfo: types,
runner: func(builder *dialog.FileBuilder) (string, error) { return builder.Save() },
callback: callback,
})
}
// LoadFile starts a dialog series to load a file.
func LoadFile(machine gui.ModalStateMachine, types []TypeInfo, callback SingleFileHandler) {
machine.SetState(&fileStartState{
machine: machine,
title: "Load",
typeInfo: types,
runner: func(builder *dialog.FileBuilder) (string, error) { return builder.Load() },
callback: callback,
})
}
type fileStartState struct {
machine gui.ModalStateMachine
title string
typeInfo []TypeInfo
runner singleFileRunner
callback SingleFileHandler
}
func (state fileStartState) HandleFiles(names []string) {
}
func (state fileStartState) Render() {
imgui.OpenPopup(state.title)
nextState := &fileWaitingState{
machine: state.machine,
title: state.title,
typeInfo: state.typeInfo,
runner: state.runner,
callback: state.callback,
}
state.machine.SetState(nextState)
}
type fileWaitingState struct {
machine gui.ModalStateMachine
title string
typeInfo []TypeInfo
runner singleFileRunner
callback SingleFileHandler
renderCount int
shouldOpenDialog bool
problem string
}
func (state *fileWaitingState) HandleFiles(names []string) {
}
func (state *fileWaitingState) Render() {
if imgui.BeginPopupModalV(state.title, nil,
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
if len(state.problem) == 0 {
imgui.Text("Please use popup dialog...")
} else {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text(state.problem)
imgui.PopStyleColor()
}
state.renderControls()
imgui.EndPopup()
state.renderCount++
if state.renderCount == 3 {
state.shouldOpenDialog = true
}
if state.shouldOpenDialog {
state.shouldOpenDialog = false
state.openDialog()
}
} else {
state.machine.SetState(nil)
}
}
func (state *fileWaitingState) renderControls() {
imgui.Separator()
if imgui.Button("Browse...") {
state.shouldOpenDialog = true
}
imgui.SameLine()
if imgui.Button("Cancel") {
state.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
}
func (state *fileWaitingState) openDialog() {
dlgBuilder := dialog.File()
for _, info := range state.typeInfo {
dlgBuilder = dlgBuilder.Filter(info.Title, info.Extensions...)
}
dlgBuilder = dlgBuilder.Filter("All files (*.*)", "*")
filename, err := state.runner(dlgBuilder)
state.machine.SetState(nil)
if err != nil {
return
}
state.reportFile(filename)
}
func (state *fileWaitingState) reportFile(filename string) {
state.problem = ""
err := state.callback(filename)
if err != nil {
state.problem = "Previous attempt failed.\nPlease check and try again.\nReason:\n" + err.Error()
state.machine.SetState(state)
}
}
+117
View File
@@ -0,0 +1,117 @@
package external
import (
"image"
"image/color"
"math"
"os"
"github.com/inkyblackness/hacked/ss1/content/audio"
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ui/gui"
)
// Import starts an import dialog series, calling the given callback with a file name.
func Import(machine gui.ModalStateMachine, info string, types []TypeInfo, callback func(string), lastFailed bool) {
machine.SetState(&importStartState{
machine: machine,
callback: callback,
info: info,
typeInfo: types,
withError: lastFailed,
})
}
// ImportAudio is a helper to handle audio file import. The callback is called with the loaded audio.
func ImportAudio(machine gui.ModalStateMachine, callback func(l8 audio.L8)) {
info := "File must be a WAV file, 22050 Hz, 8-bit or 16-bit, uncompressed."
types := []TypeInfo{{Title: "Audio files (*.wav)", Extensions: []string{"wav"}}}
var fileHandler func(string)
fileHandler = func(filename string) {
reader, err := os.Open(filename)
if err != nil {
Import(machine, info, types, fileHandler, true)
return
}
defer func() { _ = reader.Close() }()
sound, err := wav.Load(reader)
if err != nil {
Import(machine, info, types, fileHandler, true)
return
}
callback(sound)
}
Import(machine, info, types, fileHandler, false)
}
// ImportImage is a helper to handle image file import. The callback is called with the loaded image.
func ImportImage(machine gui.ModalStateMachine, paletteRetriever func() (bitmap.Palette, error), callback func(bitmap.Bitmap)) {
info := "File should be either a BMP, GIF, or a PNG file.\nPaletted images matching game palette are taken 1:1,\nothers are mapped closest fitting."
types := []TypeInfo{{Title: "Image files (*.bmp, *.gif, *.png)", Extensions: []string{"bmp", "gif", "png"}}}
var fileHandler func(string)
fileHandler = func(filename string) {
reader, err := os.Open(filename)
if err != nil {
Import(machine, "Could not open file.\n"+info, types, fileHandler, true)
return
}
defer func() { _ = reader.Close() }()
img, _, err := image.Decode(reader)
if err != nil {
Import(machine, "File not recognized as image.\n"+info, types, fileHandler, true)
return
}
var bmp bitmap.Bitmap
importMapped := true
rawPalette, err := paletteRetriever()
if err != nil {
Import(machine, "Can not import image without having a palette loaded.\n"+info, types, fileHandler, true)
return
}
if palettedImg, isPaletted := img.(image.PalettedImage); isPaletted {
imgPalette, hasPalette := palettedImg.ColorModel().(color.Palette)
if hasPalette && paletteMatches(imgPalette, rawPalette.ColorPalette(false)) {
bounds := img.Bounds()
bmp.Header.Width = int16(math.Max(0, math.Min(float64(bounds.Dx()), math.MaxInt16)))
bmp.Header.Height = int16(math.Max(0, math.Min(float64(bounds.Dy()), math.MaxInt16)))
bmp.Pixels = make([]byte, int(bmp.Header.Width)*int(bmp.Header.Height))
for row := 0; row < int(bmp.Header.Height); row++ {
for column := 0; column < int(bmp.Header.Width); column++ {
bmp.Pixels[row*int(bmp.Header.Width)+column] = palettedImg.ColorIndexAt(column, row)
}
}
importMapped = false
}
}
if importMapped {
bitmapper := bitmap.NewBitmapper(&rawPalette)
bmp = bitmapper.Map(img)
}
callback(bmp)
}
Import(machine, info, types, fileHandler, false)
}
func paletteMatches(imgPalette color.Palette, rawPalette color.Palette) bool {
if len(imgPalette) > len(rawPalette) {
return false
}
for index, clr := range imgPalette {
imgR, imgG, imgB, _ := clr.RGBA()
rawR, rawG, rawB, _ := rawPalette[index].RGBA()
if (imgR != rawR) || (imgG != rawG) || (imgB != rawB) {
return false
}
}
return true
}
+34
View File
@@ -0,0 +1,34 @@
package external
import (
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ui/gui"
)
type importStartState struct {
machine gui.ModalStateMachine
callback func(string)
info string
typeInfo []TypeInfo
withError bool
}
func (state importStartState) Render() {
imgui.OpenPopup("Import")
nextState := &importWaitingState{
machine: state.machine,
callback: state.callback,
info: state.info,
typeInfo: state.typeInfo,
}
if state.withError {
nextState.failureTime = time.Now()
}
state.machine.SetState(nextState)
}
func (state importStartState) HandleFiles(names []string) {
}
+83
View File
@@ -0,0 +1,83 @@
package external
import (
"os"
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/sqweek/dialog"
"github.com/inkyblackness/hacked/ui/gui"
)
type importWaitingState struct {
machine gui.ModalStateMachine
callback func(string)
info string
typeInfo []TypeInfo
failureTime time.Time
}
func (state *importWaitingState) Render() {
if imgui.BeginPopupModalV("Import", nil,
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
imgui.Text("Waiting for file.")
if !state.failureTime.IsZero() {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("Previous attempt failed, could not load file.\nPlease check and try again.")
imgui.PopStyleColor()
if time.Since(state.failureTime).Seconds() > 5 {
state.failureTime = time.Time{}
}
}
imgui.Text(`From your file browser drag'n'drop the file
that shall be loaded into the editor window.
`)
imgui.Text(state.info)
imgui.Separator()
if imgui.Button("Browse...") {
dlgBuilder := dialog.File()
for _, info := range state.typeInfo {
dlgBuilder = dlgBuilder.Filter(info.Title, info.Extensions...)
}
dlgBuilder = dlgBuilder.Filter("All files (*.*)", "*")
filename, err := dlgBuilder.Load()
if err == nil {
state.HandleFiles([]string{filename})
}
}
imgui.SameLine()
if imgui.Button("Cancel") {
state.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
imgui.EndPopup()
} else {
state.machine.SetState(nil)
}
}
func (state *importWaitingState) HandleFiles(names []string) {
filename, ok := state.verifyFile(names)
if ok {
state.machine.SetState(nil)
state.callback(filename)
} else {
state.failureTime = time.Now()
}
}
func (state importWaitingState) verifyFile(names []string) (string, bool) {
if len(names) != 1 {
return "", false
}
fileInfo, err := os.Stat(names[0])
if err != nil {
return "", false
}
if fileInfo.IsDir() {
return "", false
}
return names[0], true
}
+7
View File
@@ -0,0 +1,7 @@
package external
// TypeInfo describes a file.
type TypeInfo struct {
Title string
Extensions []string
}