From 51039399ec74899bca7976c324e731eeac3a9530 Mon Sep 17 00:00:00 2001 From: grimsace Date: Wed, 11 Mar 2026 13:08:40 -0500 Subject: [PATCH] Added ability to import and export town/city settings --- main.go | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++ settings.go | 12 +++- 2 files changed, 214 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index f62632f..d075930 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "archive/zip" "bytes" "compress/gzip" + "encoding/json" "fmt" "image" "image/color" @@ -806,6 +807,7 @@ func main() { } // Create UI elements for error display and action buttons + var applySettingsToUI func(*Settings) errorLabel := widget.NewLabel("") errorLabel.Wrapping = fyne.TextWrapWord errorLabel.Hide() @@ -825,6 +827,12 @@ func main() { exportMasksBtn := widget.NewButton("Export Masks", func() { showMasksSaveDialog(w, canvasImg.Image, heightmapImg.Image, bumpmapImg.Image, settings, lakes, riverMask, treeMask, roadMask, bridgeMask, wallMask, turretMask, buildingMask) }) + exportSettingsBtn := widget.NewButton("Export Settings", func() { + showSettingsExportDialog(w, settings) + }) + importSettingsBtn := widget.NewButton("Import Settings", func() { + showSettingsImportDialog(w, settings, applySettingsToUI) + }) // Main generation button and logic generateBtn = widget.NewButton("Generate", func() { go func() { @@ -1465,6 +1473,82 @@ func main() { ratioContainer.Hide() } + applySettingsToUI = func(imported *Settings) { + lastExportPath := settings.LastExportPath + imageViewState := settings.ImageViewState + + *settings = *imported + settings.LastExportPath = lastExportPath + settings.ImageViewState = imageViewState + if settings.BuildingShapeRatios == nil { + settings.BuildingShapeRatios = map[string]float64{ + "squares": 40, + "circles": 30, + "rectangles": 30, + } + } + + widthEntry.SetText(strconv.Itoa(settings.Width)) + heightEntry.SetText(strconv.Itoa(settings.Height)) + seedEntry.SetText(strconv.FormatInt(settings.Seed, 10)) + + detailSlider.value.Set(settings.Detail) + roughnessSlider.value.Set(settings.Roughness) + + lakesSlider.value.Set(float64(settings.Lakes)) + lakeSizeLowerSlider.value.Set(settings.LakeSizeLower) + lakeSizeUpperSlider.value.Set(settings.LakeSizeUpper) + lakeEdgeRoughnessSlider.value.Set(settings.LakeEdgeRoughness) + lakeShapeSelect.SetSelected(settings.LakeShape) + + riversSlider.value.Set(float64(settings.Rivers)) + minRiverWidthSlider.value.Set(settings.MinRiverWidth) + maxRiverWidthSlider.value.Set(settings.MaxRiverWidth) + riverCurvynessSlider.value.Set(settings.RiverCurvyness) + riverWidthVariabilitySlider.value.Set(settings.RiverWidthVariability) + riverEdgeRoughnessSlider.value.Set(settings.RiverEdgeRoughness) + + minTreeSizeSlider.value.Set(settings.MinTreeSize) + maxTreeSizeSlider.value.Set(settings.MaxTreeSize) + treeCoverageSlider.value.Set(settings.TreeCoverage) + treeClumpinessSlider.value.Set(settings.TreeClumpiness) + + minRoadWidthSlider.value.Set(settings.MinRoadWidth) + maxRoadWidthSlider.value.Set(settings.MaxRoadWidth) + buildingsPerRoadSlider.value.Set(float64(settings.BuildingsPerRoad)) + roadExitsSlider.value.Set(float64(settings.RoadExits)) + roadCurvynessSlider.value.Set(settings.RoadCurvyness) + roadDistributionSlider.value.Set(settings.RoadDistribution) + minRoadAngleSlider.value.Set(settings.MinRoadAngle) + + minWallWidthSlider.value.Set(settings.MinWallWidth) + maxWallWidthSlider.value.Set(settings.MaxWallWidth) + numWallsSlider.value.Set(float64(settings.NumWalls)) + cityCoverageSlider.value.Set(settings.CityCoverage) + wallCurvynessSlider.value.Set(settings.WallCurvyness) + gateSpacingSlider.value.Set(settings.GateSpacing) + showTurretsCheck.SetChecked(settings.ShowTurrets) + turretSizeSlider.value.Set(settings.TurretSize) + turretShapeSelect.SetSelected(settings.TurretShape) + turretSpacingSlider.value.Set(settings.TurretSpacing) + + numBuildingsSlider.value.Set(float64(settings.NumBuildings)) + minBuildingSizeSlider.value.Set(settings.MinBuildingSize) + maxBuildingSizeSlider.value.Set(settings.MaxBuildingSize) + buildingDistributionSlider.value.Set(settings.BuildingDistribution) + buildingShapeSelect.SetSelected(settings.BuildingShape) + updateRatioSliders() + minBuildingComplexitySlider.value.Set(float64(settings.MinBuildingComplexity)) + maxBuildingComplexitySlider.value.Set(float64(settings.MaxBuildingComplexity)) + buildingComplexityRatioSlider.value.Set(settings.BuildingComplexityRatio) + + for key := range errorStates { + errorStates[key] = false + } + errorLabel.Hide() + updateGenerateBtnState() + } + buildingsTab := container.NewTabItem("Buildings", container.NewVBox( numBuildingsSlider, minBuildingSizeSlider, @@ -1491,6 +1575,8 @@ func main() { exportHeightmapBtn, exportBumpmapBtn, exportMasksBtn, + exportSettingsBtn, + importSettingsBtn, )) // Create the main layout using a horizontal split tabs := container.NewAppTabs( @@ -1789,6 +1875,124 @@ func saveImage(img image.Image, path string) { } } +func decodeSettingsJSON(raw []byte) (*Settings, error) { + var imported Settings + if err := json.Unmarshal(raw, &imported); err != nil { + return nil, err + } + var rawKeys map[string]json.RawMessage + if err := json.Unmarshal(raw, &rawKeys); err != nil { + return nil, err + } + normalizeSettings(&imported, rawKeys) + return &imported, nil +} + +func saveSettingsToFile(path string, settings *Settings) error { + exportSettings := *settings + exportSettings.LastExportPath = "" + exportSettings.ImageViewState = 0 + + data, err := json.MarshalIndent(&exportSettings, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +func showSettingsExportDialog(win fyne.Window, settings *Settings) { + fileNameEntry := widget.NewEntry() + fileNameEntry.SetPlaceHolder("settings") + + pathLabel := widget.NewLabel(settings.LastExportPath) + + browseBtn := widget.NewButton("Browse", func() { + dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) { + if err != nil { + log.Println("Error opening folder dialog:", err) + return + } + if uri == nil { + return + } + settings.LastExportPath = uri.Path() + pathLabel.SetText(settings.LastExportPath) + }, win) + }) + + content := container.NewVBox( + widget.NewLabel("Save to:"), + container.NewBorder(nil, nil, nil, browseBtn, pathLabel), + widget.NewLabel("Filename:"), + fileNameEntry, + ) + + saveDialog := dialog.NewCustom("Export Settings", "Cancel", content, win) + + saveBtn := widget.NewButton("Save", func() { + fileName := strings.TrimSpace(fileNameEntry.Text) + if fileName == "" { + return + } + if !strings.HasSuffix(strings.ToLower(fileName), ".json") { + fileName += ".json" + } + filePath := filepath.Join(pathLabel.Text, fileName) + if err := saveSettingsToFile(filePath, settings); err != nil { + dialog.ShowError(err, win) + return + } + settings.LastExportPath = filepath.Dir(filePath) + saveDialog.Hide() + }) + saveBtn.Disable() + + fileNameEntry.OnChanged = func(text string) { + if strings.TrimSpace(text) == "" { + saveBtn.Disable() + } else { + saveBtn.Enable() + } + } + + saveDialog.SetButtons([]fyne.CanvasObject{ + widget.NewButton("Cancel", func() { + saveDialog.Hide() + }), + saveBtn, + }) + + saveDialog.Show() +} + +func showSettingsImportDialog(win fyne.Window, settings *Settings, apply func(*Settings)) { + dialog.ShowFileOpen(func(rc fyne.URIReadCloser, err error) { + if err != nil { + dialog.ShowError(err, win) + return + } + if rc == nil { + return + } + defer rc.Close() + + raw, err := io.ReadAll(rc) + if err != nil { + dialog.ShowError(err, win) + return + } + imported, err := decodeSettingsJSON(raw) + if err != nil { + dialog.ShowError(err, win) + return + } + if uri := rc.URI(); uri != nil { + settings.LastExportPath = filepath.Dir(uri.Path()) + } + apply(imported) + }, win) +} + // showSaveDialog displays a dialog for saving an image. func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) { // Create UI elements for the save dialog diff --git a/settings.go b/settings.go index f38765f..07d7565 100644 --- a/settings.go +++ b/settings.go @@ -196,6 +196,16 @@ func LoadSettings() (*Settings, error) { return nil, err } + normalizeSettings(&settings, rawKeys) + + return &settings, nil +} + +func normalizeSettings(settings *Settings, rawKeys map[string]json.RawMessage) { + if rawKeys == nil { + rawKeys = map[string]json.RawMessage{} + } + if settings.LakeShape == "" { settings.LakeShape = "circle" } @@ -322,6 +332,4 @@ func LoadSettings() (*Settings, error) { } settings.LastExportPath = homeDir } - - return &settings, nil }