Adding modding folder with a hacked build.
This commit is contained in:
+9
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,7 @@
|
||||
package external
|
||||
|
||||
// TypeInfo describes a file.
|
||||
type TypeInfo struct {
|
||||
Title string
|
||||
Extensions []string
|
||||
}
|
||||
Reference in New Issue
Block a user