527 lines
14 KiB
Go
527 lines
14 KiB
Go
package KFEditor
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/widget"
|
|
"g.r-io.lu/shynd/kemoforge/pkg/KFData/KFConfig"
|
|
"g.r-io.lu/shynd/kemoforge/pkg/KFData/KFError"
|
|
"g.r-io.lu/shynd/kemoforge/pkg/KFData/KFObjects/KFWidget/CalsWidgets"
|
|
)
|
|
|
|
type KFInfo struct {
|
|
Name string `json:"Name"`
|
|
Path string `json:"Path"`
|
|
OpenDate time.Time `json:"Last Opened"`
|
|
}
|
|
|
|
type KFProject struct {
|
|
Info KFInfo
|
|
Config *KFConfig.KFConfig
|
|
}
|
|
|
|
var (
|
|
//go:embed Templates/*
|
|
Templates embed.FS
|
|
ActiveProject *KFProject
|
|
)
|
|
|
|
func UpdateProjectInfo(info KFInfo) error {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
kemoForgeDir := homeDir + "/Documents/KemoForge"
|
|
projects, err := ReadProjectInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = os.Stat(info.Path)
|
|
if os.IsNotExist(err) {
|
|
// file does not exist so remove it from the projects.kf file
|
|
for i, project := range projects {
|
|
if project.Name == info.Name {
|
|
projects = append(projects[:i], projects[i+1:]...)
|
|
serializedProjects, err := json.Marshal(projects)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(kemoForgeDir+"/projects.kf", serializedProjects, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
projectFound := false
|
|
for i, project := range projects {
|
|
if project.Name == info.Name {
|
|
projectFound = true
|
|
projects[i] = info
|
|
serializedProjects, err := json.Marshal(projects)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(kemoForgeDir+"/projects.kf", serializedProjects, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
if !projectFound {
|
|
projects = append(projects, info)
|
|
serializedProjects, err := json.Marshal(projects)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(kemoForgeDir+"/projects.kf", serializedProjects, 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadProjectInfo reads the project info from the project file
|
|
func ReadProjectInfo() ([]KFInfo, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kemoForgeDir := homeDir + "/Documents/KemoForge"
|
|
//Check if the KemoForge directory exists
|
|
if _, err := os.Stat(kemoForgeDir); os.IsNotExist(err) {
|
|
//if it doesn't exist, create it
|
|
err = os.MkdirAll(kemoForgeDir, 0777)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
//Check if the projects.kf file exists
|
|
if _, err := os.Stat(kemoForgeDir + "/projects.kf"); os.IsNotExist(err) {
|
|
//if it doesn't exist, create it
|
|
err = os.WriteFile(kemoForgeDir+"/projects.kf", []byte("[]"), 0777)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
//Read the file
|
|
file, err := os.ReadFile(kemoForgeDir + "/projects.kf")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//unmarshal the json into a slice of structs
|
|
var projects []KFInfo
|
|
err = json.Unmarshal(file, &projects)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//Sort the projects by the last opened date
|
|
for i := 0; i < len(projects); i++ {
|
|
for j := 0; j < len(projects); j++ {
|
|
if projects[i].OpenDate.After(projects[j].OpenDate) {
|
|
temp := projects[i]
|
|
projects[i] = projects[j]
|
|
projects[j] = temp
|
|
}
|
|
}
|
|
}
|
|
|
|
//return the slice of structs
|
|
return projects, nil
|
|
}
|
|
|
|
func OpenFromInfo(info KFInfo, window fyne.Window) error {
|
|
path := info.Path
|
|
//Check if the path even exists
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
newError := UpdateProjectInfo(info)
|
|
if newError != nil {
|
|
errors.Join(err, newError, KFError.NewErrProjectNotFound(info.Name))
|
|
}
|
|
return err
|
|
}
|
|
|
|
//Check if the path ends in .KFProject
|
|
if filepath.Ext(path) != ".KFProject" {
|
|
//Check if the path is a directory and if the directory contains a .KFProject file
|
|
if _, err := os.Stat(path + "/" + filepath.Base(path) + ".KFProject"); os.IsNotExist(err) {
|
|
return KFError.NewErrProjectNotFound(info.Name)
|
|
} else {
|
|
//If it does, set the path to the directory
|
|
path = path + "/" + filepath.Base(path) + ".KFProject"
|
|
}
|
|
}
|
|
|
|
//Read the project file
|
|
file, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//Deserialize the project
|
|
project, err := Deserialize(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = project.Load(window)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func Deserialize(file []byte) (project KFInfo, err error) {
|
|
err = json.Unmarshal(file, &project)
|
|
if err != nil {
|
|
return KFInfo{}, err
|
|
}
|
|
return project, nil
|
|
}
|
|
|
|
// Load takes a deserialized project and loads it into the editor loading the scenes and functions as well
|
|
func (p KFInfo) Load(window fyne.Window) error {
|
|
Project := &KFProject{
|
|
Info: p,
|
|
}
|
|
//Walk the game directory of the project for the .KFConfig file
|
|
//If it doesn't exist, return an error
|
|
gameDir := filepath.Dir(p.Path) + "/data/"
|
|
//Look for the first file ending in .KFConfig
|
|
err := filepath.Walk(gameDir, func(path string, info os.FileInfo, err error) error {
|
|
if filepath.Ext(path) == ".KFConfig" {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = Project.Config.Load(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
err = UpdateProjectInfo(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ActiveProject = Project
|
|
// window.SetContent(CreateSceneEditor(window)) // TODO
|
|
return nil
|
|
}
|
|
|
|
func (p KFProject) Create(window fyne.Window) error {
|
|
shouldDelete := false
|
|
deletePath := ""
|
|
defer func() {
|
|
if shouldDelete && deletePath != "" {
|
|
deletePath = filepath.Clean(deletePath)
|
|
if fs.ValidPath(deletePath) {
|
|
vbox := container.NewVBox(
|
|
widget.NewLabel("An error occurred while creating the project."),
|
|
widget.NewLabel("Would you like to delete the broken project?"),
|
|
widget.NewLabel("Delete will remove: "+deletePath),
|
|
widget.NewLabel("This action cannot be undone."),
|
|
)
|
|
dialog.ShowCustomConfirm("Delete Broken Project?", "Yes", "No", vbox, func(b bool) {
|
|
if b {
|
|
err := os.RemoveAll(deletePath)
|
|
if err != nil {
|
|
dialog.ShowError(err, window)
|
|
}
|
|
}
|
|
}, window)
|
|
}
|
|
}
|
|
}()
|
|
loadingChannel := make(chan struct{})
|
|
loading := CalsWidgets.NewLoading(loadingChannel, 100*time.Millisecond, 100)
|
|
loading.SetProgress(0, "Creating Project: "+p.Config.Name)
|
|
//Pop up a dialog with a progress bar and a label that says "Creating Project"
|
|
progressDialog := dialog.NewCustomWithoutButtons("Creating Project", loading.Box, window)
|
|
progressDialog.Show()
|
|
defer progressDialog.Hide()
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kemoForgeDir := homeDir + "/Documents/KemoForge"
|
|
projectsDir := fyne.CurrentApp().Preferences().StringWithFallback("projectDir", kemoForgeDir+"/projects")
|
|
|
|
//First check if the project directory already exists
|
|
|
|
projectDir := projectsDir + "/" + p.Config.Name
|
|
projectDir = filepath.Clean(projectDir)
|
|
if !fs.ValidPath(projectDir) {
|
|
return errors.New("invalid path")
|
|
}
|
|
|
|
loading.SetProgress(10, "Checking if project already exists")
|
|
_, err = os.Stat(projectDir)
|
|
if os.IsNotExist(err) {
|
|
err = os.MkdirAll(projectDir, os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return KFError.NewErrProjectAlreadyExists(p.Config.Name)
|
|
}
|
|
|
|
//Create the project directory
|
|
loading.SetProgress(20, "Creating Project Directory")
|
|
err = os.MkdirAll(projectDir, os.ModePerm)
|
|
deletePath = projectDir
|
|
if err != nil {
|
|
shouldDelete = true
|
|
return err
|
|
}
|
|
|
|
//Create the project file
|
|
loading.SetProgress(30, "Creating Project File")
|
|
projectFilePath := projectDir + "/" + p.Config.Name + ".KFProject"
|
|
projectFilePath = filepath.Clean(projectFilePath)
|
|
if !fs.ValidPath(projectFilePath) {
|
|
shouldDelete = true
|
|
return errors.New("invalid path")
|
|
}
|
|
err = os.WriteFile(projectFilePath, p.SerializeInfo(), os.ModePerm)
|
|
if err != nil {
|
|
shouldDelete = true
|
|
return err
|
|
}
|
|
|
|
neededDirectories := []string{
|
|
"cmd/" + p.Config.Name,
|
|
"data/assets/image",
|
|
"data/assets/audio",
|
|
"data/assets/video",
|
|
"data/assets/other",
|
|
"data/scenes",
|
|
"internal/functions",
|
|
"internal/layouts",
|
|
"internal/widgets",
|
|
}
|
|
|
|
percentPerDir := 10 / len(neededDirectories)
|
|
for _, dir := range neededDirectories {
|
|
dirPath := projectDir + "/" + dir
|
|
dirPath = filepath.Clean(dirPath)
|
|
if !fs.ValidPath(dirPath) {
|
|
shouldDelete = true
|
|
return errors.New("invalid path")
|
|
}
|
|
loading.SetProgress(loading.GetProgress()+float64(percentPerDir), "Creating "+dir)
|
|
err = os.MkdirAll(dirPath, os.ModePerm)
|
|
if err != nil {
|
|
shouldDelete = true
|
|
return err
|
|
}
|
|
}
|
|
|
|
loading.SetProgress(50, "Saving Local config")
|
|
err = p.Config.Save(projectDir + "/data/Game.KFConfig")
|
|
if err != nil {
|
|
shouldDelete = true
|
|
return err
|
|
}
|
|
|
|
// templateCombo is a struct that holds the template path, destination path, and data to be used in the template
|
|
type templateCombo struct {
|
|
templatePath string
|
|
destinationPath string
|
|
data interface{}
|
|
}
|
|
|
|
// neededFiles is a slice of templateCombos that holds all the files that need to be created
|
|
var neededFiles []templateCombo
|
|
|
|
//cmd/ProjectName/ProjectName.go
|
|
mainGameData := struct {
|
|
LocalFileSystem string
|
|
LocalFunctions string
|
|
LocalLayouts string
|
|
LocalWidgets string
|
|
}{
|
|
LocalFileSystem: p.Config.Name + "/data",
|
|
LocalFunctions: p.Config.Name + "/internal/functions",
|
|
LocalLayouts: p.Config.Name + "/internal/layouts",
|
|
LocalWidgets: p.Config.Name + "/internal/widgets",
|
|
}
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/MainGame/MainGame.got",
|
|
destinationPath: projectDir + "/cmd/" + p.Config.Name + "/" + p.Config.Name + ".go",
|
|
data: mainGameData,
|
|
})
|
|
|
|
//data/FileSystem.go
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/FileLoader/FileLoader.got",
|
|
destinationPath: projectDir + "/data/FileSystem.go",
|
|
data: nil,
|
|
})
|
|
|
|
//internal/functions/CustomFunctions.go
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/CustomFunction/CustomFunction.got",
|
|
destinationPath: projectDir + "/internal/functions/CustomFunction.go",
|
|
data: nil,
|
|
})
|
|
|
|
//internal/layouts/CustomLayouts.go
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/CustomLayout/CustomLayout.got",
|
|
destinationPath: projectDir + "/internal/layouts/CustomLayout.go",
|
|
data: nil,
|
|
})
|
|
|
|
//internal/widgets/CustomWidgets.go
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/CustomWidget/CustomWidget.got",
|
|
destinationPath: projectDir + "/internal/widgets/CustomWidget.go",
|
|
data: nil,
|
|
})
|
|
|
|
//data/scenes/MainMenu.KFScene
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/Scenes/MainMenu.KFScene",
|
|
destinationPath: projectDir + "/data/scenes/MainMenu.KFScene",
|
|
data: nil,
|
|
})
|
|
|
|
//data/scenes/NewGame.KFScene
|
|
neededFiles = append(neededFiles, templateCombo{
|
|
templatePath: "Templates/Scenes/NewGame.KFScene",
|
|
destinationPath: projectDir + "/data/scenes/NewGame.KFScene",
|
|
data: nil,
|
|
})
|
|
|
|
percentPerFile := 30 / len(neededFiles)
|
|
for _, file := range neededFiles {
|
|
pathWithoutProject := strings.TrimPrefix(file.destinationPath, projectDir)
|
|
loading.SetProgress(loading.GetProgress()+float64(percentPerFile), "Creating "+pathWithoutProject)
|
|
err = MakeFromTemplate(file.templatePath, file.destinationPath, file.data)
|
|
if err != nil {
|
|
shouldDelete = true
|
|
return err
|
|
}
|
|
}
|
|
|
|
//Initialize the go mod file by running go mod init with os/exec
|
|
loading.SetProgress(80, "Initializing go mod file")
|
|
|
|
// Initialize the go mod
|
|
var stderr bytes.Buffer
|
|
cmd := exec.Command("go", "mod", "init", p.Config.Name)
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = projectDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
shouldDelete = true
|
|
log.Printf("Error initializing go mod file: %v", err)
|
|
log.Printf("Stderr: %s", stderr.String())
|
|
return errors.New("error initializing go mod file")
|
|
}
|
|
|
|
loading.SetProgress(90, "Installing fyne")
|
|
cmd = exec.Command("go", "get", "fyne.io/fyne/v2@latest")
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = projectDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
log.Printf("Error installing fyne: %v", err)
|
|
log.Printf("Stderr: %s", stderr.String())
|
|
return errors.New("error installing fyne")
|
|
}
|
|
|
|
loading.SetProgress(95, "Installing KemoForge")
|
|
cmd = exec.Command("go", "get", "g.r-io.lu/shynd/kemoforge@latest")
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = projectDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
log.Printf("Error installing KemoForge: %v", err)
|
|
log.Printf("Stderr: %s", stderr.String())
|
|
return errors.New("error installing KemoForge")
|
|
}
|
|
|
|
//Run go mod tidy
|
|
loading.SetProgress(97, "Running go mod tidy")
|
|
cmd = exec.Command("go", "mod", "tidy")
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = projectDir
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
log.Printf("Error running go mod tidy: %v", err)
|
|
log.Printf("Stderr: %s", stderr.String())
|
|
return errors.New("error running go mod tidy")
|
|
}
|
|
|
|
loading.SetProgress(100, "Project Created")
|
|
time.Sleep(1 * time.Second)
|
|
return nil
|
|
}
|
|
|
|
func (p KFProject) SerializeInfo() []byte {
|
|
//Marshal the project to JSON
|
|
serializedProject, err := json.MarshalIndent(p.Info, "", " ")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return serializedProject
|
|
}
|
|
|
|
func MakeFromTemplate(templatePath, destinationPath string, data interface{}) error {
|
|
destinationPath = filepath.Clean(destinationPath)
|
|
if !fs.ValidPath(destinationPath) {
|
|
return errors.New("invalid path")
|
|
}
|
|
_, err := fs.Stat(Templates, templatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
destinationFile, err := os.Create(destinationPath)
|
|
defer destinationFile.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t, err := template.ParseFS(Templates, templatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = t.Execute(destinationFile, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|