├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile.yml ├── Taskfile_windows.yml ├── assets.go ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── _config.yml ├── index.md ├── manifest.json ├── sdmm-example.png ├── sdmm-logo.png └── version.txt ├── go.mod ├── go.sum ├── internal ├── app │ ├── about.go │ ├── action.go │ ├── action_user.go │ ├── app.go │ ├── args.go │ ├── command │ │ ├── command.go │ │ └── storage.go │ ├── config.go │ ├── config │ │ └── config.go │ ├── config_app.go │ ├── config_preference.go │ ├── config_project.go │ ├── logs.go │ ├── prefs │ │ ├── make.go │ │ ├── prefab.go │ │ ├── prefs.go │ │ └── save.go │ ├── project.go │ ├── render │ │ ├── brush │ │ │ ├── attribute.go │ │ │ ├── batch.go │ │ │ ├── brush.go │ │ │ ├── draw.go │ │ │ ├── shader.go │ │ │ └── shape.go │ │ ├── bucket.go │ │ ├── bucket │ │ │ ├── bucket.go │ │ │ └── level │ │ │ │ ├── chunk │ │ │ │ ├── chunk.go │ │ │ │ └── unit │ │ │ │ │ └── unit.go │ │ │ │ ├── chunks.go │ │ │ │ └── level.go │ │ ├── camera.go │ │ ├── debug.go │ │ ├── overlay.go │ │ └── render.go │ ├── selfupdate │ │ ├── manifest.go │ │ └── selfupdate.go │ ├── ui │ │ ├── component │ │ │ └── component.go │ │ ├── cpenvironment │ │ │ ├── config.go │ │ │ ├── environment.go │ │ │ ├── menu.go │ │ │ ├── node.go │ │ │ ├── process.go │ │ │ └── shortcut.go │ │ ├── cpprefabs │ │ │ ├── menu.go │ │ │ ├── node.go │ │ │ ├── prefabs.go │ │ │ └── process.go │ │ ├── cpsearch │ │ │ ├── filter.go │ │ │ ├── process.go │ │ │ ├── search.go │ │ │ └── shortcut.go │ │ ├── cpvareditor │ │ │ ├── collect.go │ │ │ ├── config.go │ │ │ ├── process.go │ │ │ ├── shortcut.go │ │ │ └── vareditor.go │ │ ├── cpwsarea │ │ │ ├── config.go │ │ │ ├── logo.go │ │ │ ├── process.go │ │ │ ├── workspace │ │ │ │ ├── content.go │ │ │ │ ├── ini.go │ │ │ │ └── workspace.go │ │ │ ├── wsarea.go │ │ │ ├── wschangelog │ │ │ │ └── wschangelog.go │ │ │ ├── wscreatemap │ │ │ │ ├── save.go │ │ │ │ └── wscreatemap.go │ │ │ ├── wsempty │ │ │ │ ├── shortcut.go │ │ │ │ └── wsempty.go │ │ │ ├── wsmap │ │ │ │ ├── pmap │ │ │ │ │ ├── canvas │ │ │ │ │ │ ├── canvas.go │ │ │ │ │ │ ├── control.go │ │ │ │ │ │ ├── overlay.go │ │ │ │ │ │ └── state.go │ │ │ │ │ ├── canvas_camera.go │ │ │ │ │ ├── canvas_control.go │ │ │ │ │ ├── canvas_overlay.go │ │ │ │ │ ├── editor │ │ │ │ │ │ ├── commit.go │ │ │ │ │ │ ├── editor.go │ │ │ │ │ │ ├── instance.go │ │ │ │ │ │ ├── overlay.go │ │ │ │ │ │ ├── tile.go │ │ │ │ │ │ └── zones.go │ │ │ │ │ ├── overlay │ │ │ │ │ │ ├── color.go │ │ │ │ │ │ └── overlay.go │ │ │ │ │ ├── panel.go │ │ │ │ │ ├── panel_status.go │ │ │ │ │ ├── panel_tools.go │ │ │ │ │ ├── pmap.go │ │ │ │ │ ├── pquickedit │ │ │ │ │ │ └── pquickedit.go │ │ │ │ │ ├── psettings │ │ │ │ │ │ ├── config.go │ │ │ │ │ │ ├── map_size.go │ │ │ │ │ │ ├── psettings.go │ │ │ │ │ │ └── screenshot.go │ │ │ │ │ ├── shortcut.go │ │ │ │ │ ├── tilemenu │ │ │ │ │ │ ├── process.go │ │ │ │ │ │ ├── shortcut.go │ │ │ │ │ │ └── tilemenu.go │ │ │ │ │ ├── tools.go │ │ │ │ │ └── unit_processor.go │ │ │ │ ├── save.go │ │ │ │ ├── tools │ │ │ │ │ ├── add.go │ │ │ │ │ ├── delete.go │ │ │ │ │ ├── fill.go │ │ │ │ │ ├── grab.go │ │ │ │ │ ├── move.go │ │ │ │ │ ├── pick.go │ │ │ │ │ ├── replace.go │ │ │ │ │ ├── tool.go │ │ │ │ │ └── tools.go │ │ │ │ └── wsmap.go │ │ │ └── wsprefs │ │ │ │ ├── pref.go │ │ │ │ ├── prefs.go │ │ │ │ └── wsprefs.go │ │ ├── dialog │ │ │ ├── confirmation.go │ │ │ ├── custom.go │ │ │ ├── dialog.go │ │ │ ├── information.go │ │ │ └── simple.go │ │ ├── layout │ │ │ ├── config.go │ │ │ ├── layout.go │ │ │ └── lnode │ │ │ │ └── lnode.go │ │ ├── menu │ │ │ ├── menu.go │ │ │ ├── shortcut.go │ │ │ └── update.go │ │ └── shortcut │ │ │ ├── holder.go │ │ │ └── shortcut.go │ ├── update.go │ └── window │ │ ├── fonts.go │ │ ├── process.go │ │ ├── restart.go │ │ ├── theme.go │ │ ├── util.go │ │ └── window.go ├── dmapi │ ├── dm │ │ ├── dirs.go │ │ ├── path.go │ │ └── paths_filter.go │ ├── dmenv │ │ ├── dme.go │ │ └── object.go │ ├── dmicon │ │ ├── cache.go │ │ ├── dmi.go │ │ └── editor.go │ ├── dmmap │ │ ├── dmm.go │ │ ├── dmmap.go │ │ ├── dmmdata │ │ │ ├── dmmdata.go │ │ │ ├── dmmprefab │ │ │ │ └── prefab.go │ │ │ ├── key.go │ │ │ ├── parse.go │ │ │ ├── parse_test.go │ │ │ ├── prefabs.go │ │ │ ├── save_dm.go │ │ │ └── save_tgm.go │ │ ├── dmminstance │ │ │ └── instance.go │ │ ├── instances.go │ │ ├── storage.go │ │ └── tile.go │ ├── dmmclip │ │ └── dmmclip.go │ ├── dmmsave │ │ ├── config.go │ │ ├── dmmsave.go │ │ ├── error_code.go │ │ ├── keygen │ │ │ └── keygen.go │ │ └── save_process.go │ ├── dmmsnap │ │ └── dmmsnap.go │ └── dmvars │ │ └── variables.go ├── env │ └── env.go ├── imguiext │ ├── icon │ │ └── icon.go │ ├── imguiext.go │ ├── layout │ │ └── splitter.go │ ├── markdown │ │ └── markdown.go │ ├── style │ │ ├── colors.go │ │ └── style.go │ └── widget │ │ ├── align_text_to_frame_padding.go │ │ ├── button.go │ │ ├── custom.go │ │ ├── disabled.go │ │ ├── dummy.go │ │ ├── font.go │ │ ├── group.go │ │ ├── image.go │ │ ├── indent.go │ │ ├── input_text.go │ │ ├── input_text_multiline.go │ │ ├── input_text_with_hint.go │ │ ├── layout.go │ │ ├── line.go │ │ ├── main_menu_bar.go │ │ ├── menu.go │ │ ├── menu_item.go │ │ ├── new_line.go │ │ ├── same_line.go │ │ ├── selectable.go │ │ ├── separator.go │ │ ├── text.go │ │ ├── text_colored.go │ │ ├── text_disabled.go │ │ ├── text_frame.go │ │ ├── text_wrapped.go │ │ ├── tooltip.go │ │ ├── widget.go │ │ └── window.go ├── platform │ ├── gl.go │ ├── glfw.go │ ├── key.go │ ├── shader.go │ └── texture.go ├── req │ └── req.go ├── rsc │ ├── font.go │ ├── font │ │ ├── Inter-Medium.ttf │ │ └── icons │ │ │ ├── README.txt │ │ │ ├── icomoon.svg │ │ │ ├── icomoon.ttf │ │ │ ├── icomoon.woff │ │ │ └── selection.json │ ├── icon.ico │ ├── png.go │ ├── png │ │ ├── editor_icon.png │ │ └── editor_texture_atlas.png │ ├── rsc.go │ └── txt │ │ ├── about.txt │ │ ├── changelog-header.txt │ │ └── support.txt └── util │ ├── bounds.go │ ├── color.go │ ├── point.go │ ├── slice │ └── slice.go │ └── util.go ├── main.go ├── third_party └── sdmmparser │ ├── lib │ └── sdmmparser.h │ ├── sdmmparser.go │ └── src │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── environment.rs │ ├── icon.rs │ └── lib.rs └── versioninfo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | insert_final_newline = true 7 | 8 | [*.md] 9 | max_line_length = off 10 | 11 | [*.go] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: spair 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help reproduce and fix the issue 3 | title: "Bug: " 4 | labels: [ bug ] 5 | body: 6 | - type: input 7 | attributes: 8 | label: Version 9 | description: What version of StrongDMM did you use? 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: What happened? 15 | description: Also tell us, what did you expect to happen? 16 | placeholder: Tell us what you see! 17 | value: A bug happened! 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Reproduction 23 | description: How did you manage to make the error happen? 24 | placeholder: How did you do it? 25 | value: 1. Run StrongDMM 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Relevant log output 31 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 32 | render: bash 33 | - type: markdown 34 | attributes: 35 | value: The logs can be found with `Help -> Open Logs Folder`. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | title: "Feature Request: " 4 | labels: [ enhancement ] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Problem to be solved 9 | description: Present a concise description of the problem to be addressed by this feature request. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Sugest a solution 15 | description: A concise description of your preferred solution. 16 | - type: markdown 17 | attributes: 18 | value: | 19 | Post additional information or images if you need so. If there are multiple solutions, please present each one separately. Save comparisons for the very end. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | labels: 8 | - deps 9 | 10 | - package-ecosystem: gomod 11 | directory: / 12 | schedule: 13 | interval: weekly 14 | labels: 15 | - deps 16 | 17 | - package-ecosystem: cargo 18 | directory: ./third_party/sdmmparser/src 19 | schedule: 20 | interval: weekly 21 | labels: 22 | - deps 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | 4 | 5 | # Type of change 6 | 7 | 8 | 9 | - [ ] Minor changes or tweaks (quality of life stuff) 10 | - [ ] Bug fix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | - [ ] This change requires a documentation update 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Log file 4 | logs/ 5 | *.log 6 | 7 | # Intellij project files 8 | *.iml 9 | *.ipr 10 | *.iws 11 | .idea/ 12 | 13 | # Binaries for programs and plugins 14 | /internal/sdmm 15 | *.exe 16 | *.exe~ 17 | *.dll 18 | *.so 19 | *.dylib 20 | *.syso 21 | 22 | # Test binary, built with `go test -c` 23 | *.test 24 | 25 | # Output of the go coverage tool, specifically when used with LiteIDE 26 | *.out 27 | 28 | # Build destination folder 29 | dst/ 30 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | disable: 4 | - errcheck 5 | - govet 6 | - staticcheck 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Package", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "program": "main.go" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "third_party/sdmmparser/src/Cargo.toml" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Taskfile_windows.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | 3 | vars: 4 | GOVERSIONINFO_VERSION: v1.4.0 5 | GIT_VERSION: 6 | sh: git describe --tags --always 7 | 8 | tasks: 9 | install_goversioninfo: 10 | cmds: 11 | - go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@{{.GOVERSIONINFO_VERSION}} 12 | 13 | gen_syso: 14 | deps: 15 | - install_goversioninfo 16 | cmds: 17 | - goversioninfo -64 -platform-specific=true -file-version="{{.GIT_VERSION}}" -product-version="{{.GIT_VERSION}}" 18 | -------------------------------------------------------------------------------- /assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "sdmm/internal/rsc" 6 | ) 7 | 8 | // File is meant to be used as an "embeder" of application assets. 9 | 10 | var ( 11 | //go:embed CHANGELOG.md 12 | changelogMd string 13 | ) 14 | 15 | func init() { 16 | rsc.ChangelogMd = changelogMd 17 | } 18 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # StrongDMM 2 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StrongDMM", 3 | "version": "v2.16.0.alpha", 4 | "forceUpdate": true, 5 | "description": "Added 'Go to Definition support' to the 'Environment' tree", 6 | "downloadLinks": { 7 | "windows": "https://github.com/SpaiR/StrongDMM/releases/download/%VERSION%/strongdmm-%VERSION%-x86_64-pc-windows-gnu.exe", 8 | "linux": "https://github.com/SpaiR/StrongDMM/releases/download/%VERSION%/strongdmm-%VERSION%-x86_64-unknown-linux-gnu", 9 | "macOS": "https://github.com/SpaiR/StrongDMM/releases/download/%VERSION%/strongdmm-%VERSION%-x86_64-apple-darwin" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/sdmm-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/docs/sdmm-example.png -------------------------------------------------------------------------------- /docs/sdmm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/docs/sdmm-logo.png -------------------------------------------------------------------------------- /docs/version.txt: -------------------------------------------------------------------------------- 1 | v1.9.1 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sdmm 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/SpaiR/imgui-go v1.12.1-0.20220214190844-a0bad21e1c5d 7 | github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 8 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a 9 | github.com/go-gl/mathgl v1.2.0 10 | github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f 11 | github.com/mazznoer/csscolorparser v0.1.5 12 | github.com/minio/selfupdate v0.6.0 13 | github.com/rs/zerolog v1.34.0 14 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 15 | github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 16 | github.com/stretchr/testify v1.10.0 17 | golang.design/x/clipboard v0.7.0 18 | ) 19 | 20 | require ( 21 | aead.dev/minisign v0.2.0 // indirect 22 | github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/mattn/go-colorable v0.1.13 // indirect 25 | github.com/mattn/go-isatty v0.0.19 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | golang.org/x/crypto v0.31.0 // indirect 28 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 29 | golang.org/x/image v0.6.0 // indirect 30 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 31 | golang.org/x/sys v0.28.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /internal/app/about.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "sdmm/internal/app/ui/dialog" 7 | "sdmm/internal/env" 8 | w "sdmm/internal/imguiext/widget" 9 | "sdmm/internal/rsc" 10 | 11 | "github.com/rs/zerolog/log" 12 | "github.com/skratchdot/open-golang/open" 13 | ) 14 | 15 | var aboutText string 16 | 17 | func (a *app) openAboutWindow() { 18 | if len(aboutText) == 0 { 19 | aboutText = rsc.AboutTxt(env.Version) 20 | } 21 | dialog.Open(dialog.TypeCustom{ 22 | Title: "About", 23 | CloseButton: true, 24 | Layout: w.Layout{ 25 | w.Text(aboutText), 26 | w.NewLine(), 27 | w.AlignTextToFramePadding(), 28 | w.Text("Revision:"), 29 | w.SameLine(), 30 | w.Button(env.Revision, func() { 31 | link := fmt.Sprintf("%s/tree/%s", env.GitHub, env.Revision) 32 | log.Print("do open revision link:", link) 33 | if err := open.Run(link); err != nil { 34 | log.Print("unable to open revision link:", err) 35 | } 36 | }).Small(true), 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/args.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // Check program arguments to load dme/dmm files passed by. 9 | func (a *app) checkProgramArgs() { 10 | // The first argument is always a path to the executable. 11 | if len(os.Args) < 2 { 12 | return 13 | } 14 | 15 | var envPath string 16 | var mapPaths []string 17 | 18 | for _, arg := range os.Args { 19 | switch filepath.Ext(arg) { 20 | case ".dme": 21 | envPath = arg 22 | case ".dmm": 23 | mapPaths = append(mapPaths, arg) 24 | } 25 | } 26 | 27 | if len(envPath) > 0 { 28 | a.loadResource(envPath) 29 | } 30 | 31 | for _, mapPath := range mapPaths { 32 | a.loadResource(mapPath) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/app/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | // Used provide a unique id for every command. 4 | var commandCounter uint64 = 0 5 | 6 | type Command struct { 7 | id uint64 8 | name string 9 | 10 | undo, redo func() 11 | } 12 | 13 | func Make(name string, undo, redo func()) Command { 14 | commandCounter++ 15 | return Command{ 16 | id: commandCounter, 17 | name: name, 18 | undo: undo, 19 | redo: redo, 20 | } 21 | } 22 | 23 | func (c Command) ReadableName() string { 24 | return c.name 25 | } 26 | 27 | func (c Command) Run() Command { 28 | c.undo() 29 | return Command{ 30 | id: c.id, 31 | name: c.name, 32 | undo: c.redo, 33 | redo: c.undo, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/app/config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "sdmm/internal/app/config" 9 | 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func (a *app) ConfigRegister(cfg config.Config) { 14 | if a.configs == nil { 15 | a.configs = make(map[string]config.Config) 16 | } 17 | 18 | configFilePath := configFilePath(a.configDir, cfg.Name()) 19 | 20 | log.Printf("registering config [%s] by path [%s]...", cfg.Name(), configFilePath) 21 | 22 | // Load a raw configuration data. 23 | rawCfg := make(map[string]any) 24 | err := config.LoadV(configFilePath, &rawCfg) 25 | if err != nil { 26 | log.Print("unable to load config:", cfg.Name()) // Highly likely doesn't exist. 27 | } else { 28 | // Try to do a migration. The result var will be a nil, if there is nothing to migrate. 29 | if result, migrated := cfg.TryMigrate(rawCfg); migrated { 30 | log.Print("migrated config:", configFilePath) 31 | config.SaveV(configFilePath, result) 32 | } 33 | 34 | // Load migrated (or not) data. 35 | err = config.Load(configFilePath, cfg) 36 | if err != nil { 37 | log.Fatal().Msgf("unable to load config: %s", configFilePath) 38 | } 39 | } 40 | 41 | a.configs[cfg.Name()] = cfg 42 | 43 | log.Print("config registered:", cfg.Name()) 44 | } 45 | 46 | const backgroundSavePeriod = time.Minute * 3 47 | 48 | func (a *app) runBackgroundConfigSave() { 49 | log.Printf("background configuration save every [%s]!", backgroundSavePeriod) 50 | go func() { 51 | for range time.Tick(backgroundSavePeriod) { 52 | a.configSave() 53 | } 54 | }() 55 | } 56 | 57 | func (a *app) configSave() { 58 | _ = os.MkdirAll(a.configDir, os.ModePerm) 59 | for _, cfg := range a.configs { 60 | a.configSaveV(cfg) 61 | } 62 | } 63 | 64 | func (a *app) configSaveV(cfg config.Config) { 65 | config.Save(configFilePath(a.configDir, cfg.Name()), cfg) 66 | } 67 | 68 | func (a *app) ConfigFind(name string) config.Config { 69 | if cfg, ok := a.configs[name]; ok { 70 | return cfg 71 | } 72 | log.Fatal().Msgf("unable to find config: %s", name) 73 | return nil 74 | } 75 | 76 | func configFilePath(dir, cfgName string) string { 77 | return filepath.FromSlash(dir + "/" + cfgName + ".json") 78 | } 79 | -------------------------------------------------------------------------------- /internal/app/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | type Config interface { 11 | Name() string 12 | TryMigrate(rawCfg map[string]any) (result map[string]any, migrated bool) 13 | } 14 | 15 | func Save(filepath string, cfg Config) { 16 | SaveV(filepath, cfg) 17 | } 18 | 19 | func SaveV(filepath string, cfg any) { 20 | log.Print("saving:", filepath) 21 | f, err := os.Create(filepath) 22 | if err != nil { 23 | log.Print("unable to create file by path:", filepath) 24 | return 25 | } 26 | defer f.Close() 27 | 28 | if j, err := json.Marshal(cfg); err == nil { 29 | _, _ = f.Write(j) 30 | } else { 31 | log.Print("unable to save data by path:", filepath) 32 | } 33 | } 34 | 35 | func Load(filepath string, cfg Config) error { 36 | return LoadV(filepath, cfg) 37 | } 38 | 39 | func LoadV(filepath string, cfg any) error { 40 | log.Print("reading:", filepath) 41 | f, err := os.Open(filepath) 42 | if err != nil { 43 | return err 44 | } 45 | defer f.Close() 46 | 47 | var j []byte 48 | if j, err = os.ReadFile(filepath); err == nil { 49 | err = json.Unmarshal(j, cfg) 50 | } 51 | 52 | return err 53 | } 54 | -------------------------------------------------------------------------------- /internal/app/config_app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | const ( 6 | configName = "app" 7 | configVersion = 1 8 | ) 9 | 10 | type appConfig struct { 11 | Version uint 12 | 13 | UpdateIgnore []string 14 | } 15 | 16 | func (appConfig) Name() string { 17 | return configName 18 | } 19 | 20 | func (appConfig) TryMigrate(_ map[string]any) (result map[string]any, migrated bool) { 21 | // do nothing. yet... 22 | return nil, migrated 23 | } 24 | 25 | func (a *app) loadConfig() { 26 | a.ConfigRegister(&appConfig{ 27 | Version: configVersion, 28 | }) 29 | } 30 | 31 | func (a *app) config() *appConfig { 32 | if cfg, ok := a.ConfigFind(configName).(*appConfig); ok { 33 | return cfg 34 | } 35 | log.Fatal().Msg("can't find config") 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/logs.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sdmm/internal/util" 7 | "time" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func initializeLogs(internalDir string) string { 14 | // Configure logs directory. 15 | logDir := internalDir + "/logs" 16 | _ = os.MkdirAll(logDir, os.ModePerm) 17 | 18 | // Create log file for the current session. 19 | formattedDate := time.Now().Format(util.TimeFormat) 20 | logFile := logDir + "/" + formattedDate + ".log" 21 | file, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm) 22 | if err != nil { 23 | panic("unable to open log file") 24 | } 25 | 26 | // Attach log output to the log file and an application terminal. 27 | fileWriter := zerolog.ConsoleWriter{ 28 | Out: file, 29 | TimeFormat: time.DateTime, 30 | NoColor: true, 31 | } 32 | consoleWrite := zerolog.ConsoleWriter{ 33 | Out: os.Stdout, 34 | TimeFormat: time.DateTime, 35 | } 36 | 37 | multi := zerolog.MultiLevelWriter(fileWriter, consoleWrite) 38 | logger := zerolog.New(multi).With().Caller().Timestamp().Logger() 39 | log.Logger = logger 40 | 41 | return filepath.FromSlash(logDir) 42 | } 43 | -------------------------------------------------------------------------------- /internal/app/prefs/prefab.go: -------------------------------------------------------------------------------- 1 | package prefs 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/wsprefs" 5 | 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | type prefPrefab interface { 10 | make() any 11 | } 12 | 13 | type intPrefPrefab struct { 14 | name string 15 | desc string 16 | label string 17 | min int 18 | max int 19 | value *int 20 | post func(int) 21 | } 22 | 23 | func (p intPrefPrefab) make() any { 24 | pref := wsprefs.MakeIntPref() 25 | pref.Name = p.name 26 | pref.Desc = p.desc 27 | pref.Label = p.label 28 | pref.Min = p.min 29 | pref.Max = p.max 30 | 31 | pref.FGet = func() int { 32 | return *p.value 33 | } 34 | pref.FSet = func(value int) { 35 | log.Printf("preferences changing, [%s] to: %d", p.label, value) 36 | *p.value = value 37 | if p.post != nil { 38 | p.post(value) 39 | } 40 | } 41 | 42 | return pref 43 | } 44 | 45 | type optionPrefPrefab struct { 46 | name string 47 | desc string 48 | label string 49 | value *string 50 | post func(string) 51 | options []string 52 | help string 53 | } 54 | 55 | func (p optionPrefPrefab) make() any { 56 | pref := wsprefs.MakeOptionPref() 57 | pref.Name = p.name 58 | pref.Desc = p.desc 59 | pref.Label = p.label 60 | 61 | pref.FGet = func() string { 62 | return *p.value 63 | } 64 | pref.FSet = func(value string) { 65 | log.Printf("preferences changing, [%s] to: %s", p.label, value) 66 | *p.value = value 67 | if p.post != nil { 68 | p.post(value) 69 | } 70 | } 71 | 72 | pref.Options = p.options 73 | pref.Help = p.help 74 | 75 | return pref 76 | } 77 | 78 | type boolPrefPrefab struct { 79 | name string 80 | desc string 81 | label string 82 | value *bool 83 | post func(bool) 84 | } 85 | 86 | func (p boolPrefPrefab) make() any { 87 | pref := wsprefs.MakeBoolPref() 88 | pref.Name = p.name 89 | pref.Desc = p.desc 90 | pref.Label = p.label 91 | 92 | pref.FGet = func() bool { 93 | return *p.value 94 | } 95 | pref.FSet = func(value bool) { 96 | log.Printf("preferences changing, [%s] to: %t", p.label, value) 97 | *p.value = value 98 | if p.post != nil { 99 | p.post(value) 100 | } 101 | } 102 | 103 | return pref 104 | } 105 | -------------------------------------------------------------------------------- /internal/app/prefs/prefs.go: -------------------------------------------------------------------------------- 1 | package prefs 2 | 3 | type Prefs struct { 4 | Editor Editor 5 | Controls Controls 6 | Interface Interface 7 | Application Application 8 | } 9 | 10 | type Interface struct { 11 | Scale int 12 | Fps int 13 | } 14 | 15 | type Controls struct { 16 | AltScrollBehaviour bool 17 | QuickEditContextMenu bool 18 | QuickEditMapPane bool 19 | } 20 | 21 | type Editor struct { 22 | SaveFormat string 23 | CodeEditor string 24 | NudgeMode string 25 | SanitizeVariables bool 26 | } 27 | 28 | type Application struct { 29 | CheckForUpdates bool 30 | AutoUpdate bool 31 | } 32 | -------------------------------------------------------------------------------- /internal/app/prefs/save.go: -------------------------------------------------------------------------------- 1 | package prefs 2 | 3 | const ( 4 | SaveFormatInitial = "Initial" 5 | SaveFormatTGM = "TGM" 6 | SaveFormatDMM = "DMM" 7 | 8 | SaveFormatHelp = `Initial - the map will be saved in the format in which it was loaded 9 | TGM - a custom map format made by TG, helps to make map file more readable and reduce merge conflicts 10 | DMM - a default map format used by the DM map editor 11 | ` 12 | ) 13 | 14 | var SaveFormats = []string{ 15 | SaveFormatInitial, 16 | SaveFormatTGM, 17 | SaveFormatDMM, 18 | } 19 | 20 | const ( 21 | CodeEditorVSC = "Visual Studio Code" 22 | CodeEditorDM = "Dreammaker" 23 | CodeEditorNPP = "Notepad++" 24 | CodeEditorDefault = "Default App" 25 | 26 | CodeEditorVSCActual = "code" 27 | CodeEditorDMActual = "dreammaker" 28 | CodeEditorNPPActual = "notepad++" 29 | 30 | CodeEditorHelp = `These programs must be present in your system PATH to work when using Go to Definition on a prefab. 31 | Default App and currently Dreammaker can only open to the relevant file, not the specific line number.` 32 | ) 33 | 34 | var CodeEditors = []string{ 35 | CodeEditorVSC, 36 | CodeEditorDM, 37 | CodeEditorNPP, 38 | CodeEditorDefault, 39 | } 40 | 41 | const ( 42 | SaveNudgeModePixel = "pixel_x/pixel_y" 43 | SaveNudgeModeStep = "step_x/step_y" 44 | SaveNudgeModePixelAlt = "pixel_w/pixel_z" 45 | ) 46 | 47 | var SaveNudgeModes = []string{ 48 | SaveNudgeModePixel, 49 | SaveNudgeModeStep, 50 | SaveNudgeModePixelAlt, 51 | } 52 | -------------------------------------------------------------------------------- /internal/app/render/brush/attribute.go: -------------------------------------------------------------------------------- 1 | package brush 2 | 3 | type attribute struct { 4 | size int32 5 | xtype uint32 6 | xtypeSize int32 7 | normalized bool 8 | } 9 | 10 | type attributesList struct { 11 | stride int32 12 | attrs []attribute 13 | } 14 | 15 | func (a *attributesList) addAttribute(attribute attribute) { 16 | a.attrs = append(a.attrs, attribute) 17 | a.stride += attribute.xtypeSize * attribute.size 18 | } 19 | -------------------------------------------------------------------------------- /internal/app/render/brush/batch.go: -------------------------------------------------------------------------------- 1 | package brush 2 | 3 | type modeType int 4 | 5 | const ( 6 | mtRect modeType = iota 7 | mtLine 8 | ) 9 | 10 | type Batching struct { 11 | data []float32 12 | calls []batchCall 13 | 14 | mode modeType 15 | 16 | idx uint32 17 | indices []uint32 18 | 19 | texture uint32 20 | len int32 21 | offset int 22 | } 23 | 24 | func (b *Batching) flush() { 25 | if b.len != 0 && len(b.indices) > 0 { 26 | b.calls = append(b.calls, batchCall{ 27 | texture: b.texture, 28 | len: b.len, 29 | offset: b.offset, 30 | mode: b.mode, 31 | }) 32 | 33 | b.offset += int(b.len) * 4 // 32 bits = 4 bytes; Offset is number of bytes per buffer. 34 | b.len = 0 35 | b.texture = 0 36 | } 37 | } 38 | 39 | func (b *Batching) clear() { 40 | b.data = b.data[:0] 41 | b.calls = b.calls[:0] 42 | 43 | b.mode = 0 44 | 45 | b.idx = 0 46 | b.indices = b.indices[:0] 47 | 48 | b.texture = 0 49 | b.offset = 0 50 | b.len = 0 51 | } 52 | 53 | var batching *Batching 54 | 55 | func init() { 56 | batching = &Batching{} 57 | } 58 | 59 | type batchCall struct { 60 | texture uint32 61 | len int32 62 | offset int 63 | mode modeType 64 | } 65 | -------------------------------------------------------------------------------- /internal/app/render/brush/brush.go: -------------------------------------------------------------------------------- 1 | package brush 2 | 3 | import ( 4 | "sdmm/internal/platform" 5 | 6 | "github.com/go-gl/gl/v3.3-core/gl" 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | var ( 11 | initialized bool 12 | 13 | attrsList attributesList 14 | 15 | program uint32 16 | 17 | vao uint32 18 | vbo uint32 19 | ebo uint32 20 | 21 | uniformLocationTransform int32 22 | uniformLocationHasTexture int32 23 | ) 24 | 25 | func TryInit() { 26 | if !initialized { 27 | initialized = true 28 | 29 | log.Print("initializing...") 30 | 31 | attrsList.addAttribute(attribute{ 32 | size: 2, 33 | xtype: gl.FLOAT, 34 | xtypeSize: platform.FloatSize, 35 | normalized: false, 36 | }) 37 | attrsList.addAttribute(attribute{ 38 | size: 4, 39 | xtype: gl.FLOAT, 40 | xtypeSize: platform.FloatSize, 41 | normalized: false, 42 | }) 43 | attrsList.addAttribute(attribute{ 44 | size: 2, 45 | xtype: gl.FLOAT, 46 | xtypeSize: platform.FloatSize, 47 | normalized: false, 48 | }) 49 | 50 | initShader(vertexShader(), fragmentShader()) 51 | initBuffers() 52 | initAttributes() 53 | 54 | log.Print("initialized") 55 | } 56 | } 57 | 58 | func Dispose() { 59 | log.Print("disposing...") 60 | gl.DeleteVertexArrays(1, &vao) 61 | gl.DeleteBuffers(1, &vbo) 62 | gl.DeleteBuffers(1, &ebo) 63 | log.Print("disposed") 64 | } 65 | 66 | func initShader(vertex, fragment string) { 67 | log.Print("initializing shader...") 68 | var err error 69 | if program, err = platform.NewShaderProgram(vertex, fragment); err != nil { 70 | log.Fatal().Msgf("unable to create shader: %v", err) 71 | } 72 | 73 | uniformLocationTransform = gl.GetUniformLocation(program, gl.Str("Transform\x00")) 74 | uniformLocationHasTexture = gl.GetUniformLocation(program, gl.Str("HasTexture\x00")) 75 | 76 | log.Print("shader initialized") 77 | } 78 | 79 | func initBuffers() { 80 | log.Print("initializing buffers...") 81 | gl.GenVertexArrays(1, &vao) 82 | gl.GenBuffers(1, &vbo) 83 | gl.GenBuffers(1, &ebo) 84 | log.Print("buffers initialized") 85 | } 86 | 87 | func initAttributes() { 88 | gl.BindVertexArray(vao) 89 | gl.BindBuffer(gl.ARRAY_BUFFER, vbo) 90 | 91 | var offset int32 92 | for idx, attr := range attrsList.attrs { 93 | gl.EnableVertexAttribArray(uint32(idx)) 94 | gl.VertexAttribPointerWithOffset(uint32(idx), attr.size, attr.xtype, attr.normalized, attrsList.stride, uintptr(offset)) 95 | offset += attr.size * attr.xtypeSize 96 | } 97 | 98 | gl.BindBuffer(gl.ARRAY_BUFFER, 0) 99 | gl.BindVertexArray(0) 100 | } 101 | -------------------------------------------------------------------------------- /internal/app/render/brush/draw.go: -------------------------------------------------------------------------------- 1 | package brush 2 | 3 | import ( 4 | "sdmm/internal/platform" 5 | 6 | "github.com/go-gl/gl/v3.3-core/gl" 7 | "github.com/go-gl/mathgl/mgl32" 8 | ) 9 | 10 | func Draw(w, h, x, y, z float32) { 11 | // Ensure that the latest batch state is persisted. 12 | batching.flush() 13 | 14 | // No data to draw. 15 | if len(batching.data) == 0 { 16 | return 17 | } 18 | 19 | gl.UseProgram(program) 20 | gl.BindVertexArray(vao) 21 | 22 | mtxTransform := transformationMatrix(w, h, x, y, z) 23 | gl.UniformMatrix4fv(uniformLocationTransform, 1, false, &mtxTransform[0]) 24 | 25 | gl.BindBuffer(gl.ARRAY_BUFFER, vbo) 26 | gl.BufferData(gl.ARRAY_BUFFER, len(batching.data)*platform.FloatSize, gl.Ptr(batching.data), gl.STREAM_DRAW) 27 | gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo) 28 | gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(batching.indices)*platform.FloatSize, gl.Ptr(batching.indices), gl.STREAM_DRAW) 29 | 30 | for _, c := range batching.calls { 31 | if c.texture != 0 { 32 | gl.Uniform1i(uniformLocationHasTexture, 1) 33 | gl.BindTexture(gl.TEXTURE_2D, c.texture) 34 | } else { 35 | gl.Uniform1i(uniformLocationHasTexture, 0) 36 | } 37 | 38 | switch c.mode { 39 | case mtRect: 40 | gl.DrawElementsWithOffset(gl.TRIANGLES, c.len, gl.UNSIGNED_INT, uintptr(c.offset)) 41 | case mtLine: 42 | gl.DrawElementsWithOffset(gl.LINES, c.len, gl.UNSIGNED_INT, uintptr(c.offset)) 43 | } 44 | } 45 | 46 | gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0) 47 | gl.BindBuffer(gl.ARRAY_BUFFER, 0) 48 | gl.BindVertexArray(0) 49 | gl.UseProgram(0) 50 | 51 | // Clear batch state. 52 | batching.clear() 53 | } 54 | 55 | func transformationMatrix(w, h, x, y, z float32) mgl32.Mat4 { 56 | view := mgl32.Ortho(0, w, 0, h, -1, 1) 57 | scale := mgl32.Scale2D(z, z).Mat4() 58 | shift := mgl32.Translate2D(x, y).Mat4() 59 | return view.Mul4(scale).Mul4(shift) 60 | } 61 | -------------------------------------------------------------------------------- /internal/app/render/brush/shader.go: -------------------------------------------------------------------------------- 1 | package brush 2 | 3 | func vertexShader() string { 4 | return ` 5 | #version 330 core 6 | 7 | uniform mat4 Transform; 8 | 9 | layout (location = 0) in vec2 in_pos; 10 | layout (location = 1) in vec4 in_color; 11 | layout (location = 2) in vec2 in_texture_uv; 12 | 13 | out vec2 frag_texture_uv; 14 | out vec4 frag_color; 15 | 16 | void main() { 17 | frag_texture_uv = in_texture_uv; 18 | frag_color = in_color; 19 | gl_Position = Transform * vec4(in_pos, 1, 1); 20 | } 21 | ` + "\x00" 22 | } 23 | 24 | func fragmentShader() string { 25 | return ` 26 | #version 330 core 27 | 28 | uniform sampler2D Texture; 29 | uniform bool HasTexture; 30 | 31 | in vec2 frag_texture_uv; 32 | in vec4 frag_color; 33 | 34 | out vec4 outputColor; 35 | 36 | void main() { 37 | if (HasTexture) { 38 | outputColor = frag_color * texture(Texture, frag_texture_uv); 39 | } else { 40 | outputColor = frag_color; 41 | } 42 | } 43 | ` + "\x00" 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/render/brush/shape.go: -------------------------------------------------------------------------------- 1 | package brush 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmicon" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | const ( 9 | rectVerticesLen = 4 // Rect contains of 4 vertices. 10 | rectIndicesLen = 6 // Rect contains of 6 indices. 11 | 12 | lineVerticesLen = 2 // Line contains of 2 vertices. 13 | lineIndicesLen = 2 // Line contains of 2 indices. 14 | ) 15 | 16 | func RectTextured(x1, y1, x2, y2 float32, col util.Color, texture uint32, u1, v1, u2, v2 float32) { 17 | RectTexturedV(x1, y1, x2, y2, col.R(), col.G(), col.B(), col.A(), texture, u1, v1, u2, v2) 18 | } 19 | 20 | func RectTexturedV(x1, y1, x2, y2, r, g, b, a float32, texture uint32, u1, v1, u2, v2 float32) { 21 | if batching.mode != mtRect || batching.texture != texture { 22 | batching.flush() 23 | } 24 | 25 | batching.texture = texture 26 | batching.mode = mtRect 27 | 28 | batching.data = append(batching.data, 29 | x1, y1, r, g, b, a, u1, v2, // bottom-left 30 | x2, y1, r, g, b, a, u2, v2, // bottom-right 31 | x1, y2, r, g, b, a, u1, v1, // top-left 32 | x2, y2, r, g, b, a, u2, v1, // top-right 33 | ) 34 | 35 | batching.indices = append(batching.indices, 36 | batching.idx+0, batching.idx+1, batching.idx+2, // bottom-left triangle 37 | batching.idx+1, batching.idx+3, batching.idx+2, // top-right triangle 38 | ) 39 | 40 | batching.idx += rectVerticesLen 41 | batching.len += rectIndicesLen 42 | } 43 | 44 | func RectFilled(x1, y1, x2, y2 float32, col util.Color) { 45 | RectFilledV(x1, y1, x2, y2, col.R(), col.G(), col.B(), col.A()) 46 | } 47 | 48 | func RectFilledV(x1, y1, x2, y2, r, g, b, a float32) { 49 | s := dmicon.WhiteRect() 50 | RectTexturedV(x1, y1, x2, y2, r, g, b, a, s.Texture(), s.U1, s.V1, s.U2, s.V2) 51 | } 52 | 53 | func Rect(x1, y1, x2, y2 float32, col util.Color) { 54 | RectV(x1, y1, x2, y2, col.R(), col.G(), col.B(), col.A()) 55 | } 56 | 57 | func RectV(x1, y1, x2, y2, r, g, b, a float32) { 58 | LineV(x1, y1, x2, y1, r, g, b, a) 59 | LineV(x2, y1, x2, y2, r, g, b, a) 60 | LineV(x2, y2, x1, y2, r, g, b, a) 61 | LineV(x1, y2, x1, y1, r, g, b, a) 62 | } 63 | 64 | func Line(x1, y1, x2, y2 float32, col util.Color) { 65 | LineV(x1, y1, x2, y2, col.R(), col.G(), col.B(), col.A()) 66 | } 67 | 68 | func LineV(x1, y1, x2, y2, r, g, b, a float32) { 69 | if batching.mode != mtLine { 70 | batching.flush() 71 | } 72 | 73 | batching.mode = mtLine 74 | 75 | batching.data = append(batching.data, 76 | x1, y1, r, g, b, a, 0, 0, // first point 77 | x2, y2, r, g, b, a, 0, 0, // second point 78 | ) 79 | 80 | batching.indices = append(batching.indices, 81 | batching.idx+0, batching.idx+1, 82 | ) 83 | 84 | batching.idx += lineVerticesLen 85 | batching.len += lineIndicesLen 86 | } 87 | -------------------------------------------------------------------------------- /internal/app/render/bucket.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "sdmm/internal/app/render/brush" 5 | "sdmm/internal/app/render/bucket/level/chunk/unit" 6 | "sdmm/internal/util" 7 | ) 8 | 9 | var ( 10 | MultiZRendering = true 11 | 12 | multiZShadow = util.MakeColor(0, 0, 0, .35) 13 | ) 14 | 15 | type unitProcessor interface { 16 | ProcessUnit(unit.Unit) (visible bool) 17 | } 18 | 19 | func (r *Render) batchBucketUnits(viewBounds util.Bounds) { 20 | if MultiZRendering && r.Camera.Level > 1 { 21 | for level := 1; level < r.Camera.Level; level++ { 22 | r.batchLevel(level, viewBounds, false) // Draw everything below. 23 | } 24 | 25 | // Draw a "shadow" overlay to visually separate different levels. 26 | brush.RectFilled(viewBounds.X1, viewBounds.Y1, viewBounds.X2, viewBounds.Y2, multiZShadow) 27 | } 28 | 29 | r.batchLevel(r.Camera.Level, viewBounds, true) // Draw currently visible level. 30 | 31 | if r.overlay != nil { 32 | r.overlay.FlushUnits() 33 | } 34 | } 35 | 36 | func (r *Render) batchLevel(level int, viewBounds util.Bounds, withUnitHighlight bool) { 37 | visibleLevel := r.bucket.Level(level) 38 | 39 | // Iterate through every layer to render. 40 | for _, layer := range visibleLevel.Layers { 41 | // Iterate through chunks with units on the rendered layer. 42 | for _, chunk := range visibleLevel.ChunksByLayers[layer] { 43 | // Out of bounds = skip. 44 | if !chunk.ViewBounds.ContainsV(viewBounds) { 45 | continue 46 | } 47 | 48 | // Get all units in the chunk for the specific layer. 49 | for _, u := range chunk.UnitsByLayers[layer] { 50 | // Out of bounds = skip 51 | if !u.ViewBounds().ContainsV(viewBounds) { 52 | continue 53 | } 54 | // Process unit 55 | if r.unitProcessor != nil && !r.unitProcessor.ProcessUnit(u) { 56 | continue 57 | } 58 | 59 | brush.RectTexturedV( 60 | u.ViewBounds().X1, u.ViewBounds().Y1, u.ViewBounds().X2, u.ViewBounds().Y2, 61 | u.R(), u.G(), u.B(), u.A(), 62 | u.Sprite().Texture(), 63 | u.Sprite().U1, u.Sprite().V1, u.Sprite().U2, u.Sprite().V2, 64 | ) 65 | 66 | if withUnitHighlight { 67 | r.batchUnitHighlight(u) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | func (r *Render) batchUnitHighlight(u unit.Unit) { 75 | if r.overlay == nil { 76 | return 77 | } 78 | if highlight := r.overlay.Units()[u.Instance().Id()]; highlight != nil { 79 | r, g, b, a := highlight.Color().RGBA() 80 | brush.RectTexturedV( 81 | u.ViewBounds().X1, u.ViewBounds().Y1, u.ViewBounds().X2, u.ViewBounds().Y2, 82 | r, g, b, a, 83 | u.Sprite().Texture(), 84 | u.Sprite().U1, u.Sprite().V1, u.Sprite().U2, u.Sprite().V2, 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/app/render/bucket/bucket.go: -------------------------------------------------------------------------------- 1 | package bucket 2 | 3 | import ( 4 | "sort" 5 | 6 | "sdmm/internal/app/render/bucket/level" 7 | "sdmm/internal/dmapi/dmmap" 8 | "sdmm/internal/util" 9 | 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | // Bucket contains data needed to render the map. 14 | // The Bucket itself is made of Level's which are made of Chunk's. 15 | type Bucket struct { 16 | Levels []int 17 | levels map[int]*level.Level 18 | } 19 | 20 | func New() *Bucket { 21 | return &Bucket{ 22 | levels: make(map[int]*level.Level), 23 | } 24 | } 25 | 26 | // UpdateLevel updates a specific level of the bucket. If the level not exist, will create it at first. 27 | func (b *Bucket) UpdateLevel(dmm *dmmap.Dmm, levelValue int, tilesToUpdate []util.Point) { 28 | log.Printf("updating bucket with [%s]...", dmm.Path.Readable) 29 | b.getOrCreateLevel(dmm, levelValue).Update(dmm, tilesToUpdate) 30 | log.Print("bucket updated") 31 | } 32 | 33 | // Level returns a specific level of the bucket or nil if it's not exist. 34 | func (b *Bucket) Level(level int) *level.Level { 35 | return b.levels[level] 36 | } 37 | 38 | func (b *Bucket) getOrCreateLevel(dmm *dmmap.Dmm, levelValue int) *level.Level { 39 | if l, ok := b.levels[levelValue]; ok { 40 | return l 41 | } else { 42 | log.Print("created level:", levelValue) 43 | l = level.New(dmm, levelValue) 44 | b.Levels = append(b.Levels, levelValue) 45 | sort.Ints(b.Levels) 46 | b.levels[levelValue] = l 47 | return l 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/app/render/bucket/level/chunk/chunk.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | import ( 4 | "sdmm/internal/app/render/bucket/level/chunk/unit" 5 | "sdmm/internal/dmapi/dmmap" 6 | "sdmm/internal/util" 7 | 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | // Size is a maximum number of tiles per axis for a single Chunk. 12 | const Size = 24 13 | 14 | // Chunk stores the actual data to render. 15 | // It stores two types of bounds: view and map. 16 | // View bounds are a visual bounds which are used to ignore the chunk if it's out of the user viewport. 17 | // Map bounds are coordinate points of tiles in the chunk. 18 | type Chunk struct { 19 | ViewBounds, MapBounds util.Bounds 20 | 21 | UnitsByLayers map[float32][]unit.Unit 22 | } 23 | 24 | func New(x1, y1, x2, y2, iconSize float32) *Chunk { 25 | return &Chunk{ 26 | ViewBounds: util.Bounds{ 27 | X1: (x1 - 1) * iconSize, 28 | Y1: (y1 - 1) * iconSize, 29 | X2: x2 * iconSize, 30 | Y2: y2 * iconSize, 31 | }, 32 | MapBounds: util.Bounds{ 33 | X1: x1, 34 | Y1: y1, 35 | X2: x2, 36 | Y2: y2, 37 | }, 38 | } 39 | } 40 | 41 | // Update will update internal data of the current chunk. 42 | // Basically, we will create units for every tile in the chunk. 43 | func (c *Chunk) Update(dmm *dmmap.Dmm, level int) { 44 | // Create a storage for our units by Layers with initial capacity. 45 | // Inner slices are created with initial capacity as well. 46 | unitsByLayers := make(map[float32][]unit.Unit, len(c.UnitsByLayers)) 47 | for layer := range c.UnitsByLayers { 48 | unitsByLayers[layer] = make([]unit.Unit, 0, len(c.UnitsByLayers[layer])) 49 | } 50 | 51 | for x := c.MapBounds.X1; x <= c.MapBounds.X2; x++ { 52 | for y := c.MapBounds.Y1; y <= c.MapBounds.Y2; y++ { 53 | x, y := int(x), int(y) 54 | for _, i := range dmm.GetTile(util.Point{X: x, Y: y, Z: level}).Instances() { 55 | u := unit.Make(x, y, i, dmmap.WorldIconSize) 56 | unitsByLayers[u.Layer()] = append(unitsByLayers[u.Layer()], u) 57 | } 58 | } 59 | } 60 | 61 | c.UnitsByLayers = unitsByLayers 62 | log.Printf("chunk level [%d] updated: %v", level, c.MapBounds) 63 | } 64 | -------------------------------------------------------------------------------- /internal/app/render/camera.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | type Camera struct { 4 | Scale float32 5 | ShiftX float32 6 | ShiftY float32 7 | 8 | // Level stores currently visible Z-level of the map. 9 | Level int 10 | } 11 | 12 | func newCamera() *Camera { 13 | return &Camera{Scale: 1, Level: 1} 14 | } 15 | 16 | func (s *Camera) Translate(x, y float32) { 17 | s.ShiftX += x 18 | s.ShiftY += y 19 | } 20 | 21 | func (s *Camera) Zoom(zoomIn bool, scaleFactor float32) { 22 | if zoomIn { 23 | s.Scale *= scaleFactor 24 | } else { 25 | s.Scale /= scaleFactor 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/render/debug.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "sdmm/internal/app/render/brush" 7 | "sdmm/internal/util" 8 | ) 9 | 10 | var chunkColors map[util.Bounds]util.Color //nolint:unused 11 | 12 | // Debug method to render chunks borders. 13 | // 14 | //nolint:unused 15 | func (r *Render) batchChunksVisuals() { 16 | if chunkColors == nil { 17 | println("[debug] CHUNKS VISUALISATION ENABLED!") 18 | chunkColors = make(map[util.Bounds]util.Color) 19 | } 20 | 21 | visibleLevel := r.bucket.Level(r.Camera.Level) 22 | 23 | for _, c := range visibleLevel.Chunks { 24 | var chunkColor util.Color 25 | if color, ok := chunkColors[c.MapBounds]; ok { 26 | chunkColor = color 27 | } else { 28 | chunkColor = util.MakeColor(rand.Float32(), rand.Float32(), rand.Float32(), .25) 29 | chunkColors[c.MapBounds] = chunkColor 30 | } 31 | 32 | brush.RectFilled(c.ViewBounds.X1, c.ViewBounds.Y1, c.ViewBounds.X2, c.ViewBounds.Y2, chunkColor) 33 | brush.RectV(c.ViewBounds.X1, c.ViewBounds.Y1, c.ViewBounds.X2, c.ViewBounds.Y2, 1, 1, 1, .5) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/app/render/overlay.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "sdmm/internal/app/render/brush" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | type OverlayArea interface { 9 | Bounds() util.Bounds 10 | FillColor() util.Color 11 | BorderColor() util.Color 12 | } 13 | 14 | type HighlightUnit interface { 15 | Id() uint64 16 | Color() util.Color 17 | } 18 | 19 | type AreaBorder interface { 20 | Borders() []util.Bounds 21 | Color() util.Color 22 | } 23 | 24 | type overlay interface { 25 | Areas() []OverlayArea 26 | FlushAreas() 27 | 28 | Units() map[uint64]HighlightUnit 29 | FlushUnits() 30 | 31 | AreasBorders() []AreaBorder 32 | FlushAreasBorders() 33 | } 34 | 35 | // Draw overlays for aras borders. 36 | func (r *Render) batchOverlayAreasBorders() { 37 | if r.overlay == nil { 38 | return 39 | } 40 | 41 | for _, areaBorder := range r.overlay.AreasBorders() { 42 | for _, bounds := range areaBorder.Borders() { 43 | brush.Line(bounds.X1, bounds.Y1, bounds.X2, bounds.Y2, areaBorder.Color()) 44 | } 45 | } 46 | 47 | r.overlay.FlushAreasBorders() 48 | } 49 | 50 | // Draw an overlay for the map tiles. 51 | func (r *Render) batchOverlayAreas() { 52 | if r.overlay == nil { 53 | return 54 | } 55 | 56 | for _, a := range r.overlay.Areas() { 57 | brush.RectFilled(a.Bounds().X1, a.Bounds().Y1, a.Bounds().X2, a.Bounds().Y2, a.FillColor()) 58 | brush.Rect(a.Bounds().X1, a.Bounds().Y1, a.Bounds().X2, a.Bounds().Y2, a.BorderColor()) 59 | } 60 | 61 | r.overlay.FlushAreas() 62 | } 63 | -------------------------------------------------------------------------------- /internal/app/render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "sdmm/internal/app/render/brush" 5 | "sdmm/internal/app/render/bucket" 6 | "sdmm/internal/dmapi/dmmap" 7 | "sdmm/internal/util" 8 | 9 | "github.com/go-gl/gl/v3.3-core/gl" 10 | ) 11 | 12 | type Render struct { 13 | Camera *Camera 14 | 15 | bucket *bucket.Bucket 16 | 17 | overlay overlay 18 | unitProcessor unitProcessor 19 | } 20 | 21 | func New() *Render { 22 | brush.TryInit() 23 | return &Render{ 24 | Camera: newCamera(), 25 | bucket: bucket.New(), 26 | } 27 | } 28 | 29 | func (r *Render) SetUnitProcessor(processor unitProcessor) { 30 | r.unitProcessor = processor 31 | } 32 | 33 | func (r *Render) SetOverlay(state overlay) { 34 | r.overlay = state 35 | } 36 | 37 | func (r *Render) SetActiveLevel(dmm *dmmap.Dmm, activeLevel int) { 38 | r.Camera.Level = activeLevel 39 | if r.bucket.Level(activeLevel) == nil { // Ensure level exists 40 | r.UpdateBucket(dmm, activeLevel) 41 | } 42 | } 43 | 44 | // UpdateBucketV will update the bucket data by the provided level. 45 | func (r *Render) UpdateBucketV(dmm *dmmap.Dmm, level int, tilesToUpdate []util.Point) { 46 | r.bucket.UpdateLevel(dmm, level, tilesToUpdate) 47 | } 48 | 49 | // UpdateBucket will ensure that the bucket has data by the provided level. 50 | func (r *Render) UpdateBucket(dmm *dmmap.Dmm, level int) { 51 | r.UpdateBucketV(dmm, level, nil) 52 | } 53 | 54 | func (r *Render) Draw(width, height float32) { 55 | r.prepare() 56 | r.draw(width, height) 57 | r.cleanup() 58 | } 59 | 60 | // Initialize OpenGL state. 61 | func (r *Render) prepare() { 62 | gl.Enable(gl.BLEND) 63 | gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) 64 | gl.BlendEquation(gl.FUNC_ADD) 65 | gl.ActiveTexture(gl.TEXTURE0) 66 | } 67 | 68 | func (r *Render) draw(width, height float32) { 69 | r.batchBucketUnits(r.viewportBounds(width, height)) 70 | //r.batchChunksVisuals() 71 | r.batchOverlayAreasBorders() 72 | r.batchOverlayAreas() 73 | brush.Draw(width, height, r.Camera.ShiftX, r.Camera.ShiftY, r.Camera.Scale) 74 | } 75 | 76 | // Clean OpenGL state after rendering. 77 | func (r *Render) cleanup() { 78 | gl.Disable(gl.BLEND) 79 | } 80 | 81 | func (r *Render) viewportBounds(width, height float32) util.Bounds { 82 | // Get transformed bounds of the map, so we can ignore out of bounds units. 83 | w := width / r.Camera.Scale 84 | h := height / r.Camera.Scale 85 | 86 | x1 := -r.Camera.ShiftX 87 | y1 := -r.Camera.ShiftY 88 | x2 := x1 + w 89 | y2 := y1 + h 90 | 91 | return util.Bounds{ 92 | X1: x1, 93 | Y1: y1, 94 | X2: x2, 95 | Y2: y2, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/app/selfupdate/manifest.go: -------------------------------------------------------------------------------- 1 | package selfupdate 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "sdmm/internal/env" 9 | "sdmm/internal/req" 10 | ) 11 | 12 | type Manifest struct { 13 | Name string `json:"name"` 14 | Version string `json:"version"` 15 | Description string `json:"description"` 16 | DownloadLinks DownloadLinks `json:"downloadLinks"` 17 | } 18 | 19 | type DownloadLinks struct { 20 | Windows string `json:"windows"` 21 | Linux string `json:"linux"` 22 | MacOS string `json:"macOS"` 23 | } 24 | 25 | const ( 26 | placeholderVersion = "%VERSION%" 27 | ) 28 | 29 | func ParseManifest(data []byte) (manifest Manifest, err error) { 30 | if err = json.Unmarshal(data, &manifest); err != nil { 31 | return Manifest{}, err 32 | } 33 | 34 | // Replace version placeholder with the actual value. 35 | replaceVersionPlaceholder(&manifest.DownloadLinks.Windows, manifest.Version) 36 | replaceVersionPlaceholder(&manifest.DownloadLinks.Linux, manifest.Version) 37 | replaceVersionPlaceholder(&manifest.DownloadLinks.MacOS, manifest.Version) 38 | 39 | return manifest, nil 40 | } 41 | 42 | func replaceVersionPlaceholder(url *string, version string) { 43 | *url = strings.ReplaceAll(*url, placeholderVersion, version) 44 | } 45 | 46 | func FetchRemoteManifest() (Manifest, error) { 47 | if manifestData, err := req.Get(env.Manifest); err == nil { 48 | return ParseManifest(manifestData) 49 | } else { 50 | return Manifest{}, fmt.Errorf("unable to get manifest data: %w", err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/app/selfupdate/selfupdate.go: -------------------------------------------------------------------------------- 1 | package selfupdate 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/minio/selfupdate" 8 | ) 9 | 10 | func Update(build []byte) error { 11 | if err := selfupdate.Apply(bytes.NewReader(build), selfupdate.Options{}); err != nil { 12 | return fmt.Errorf("unable to self-update: %w", err) 13 | } 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/app/ui/component/component.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | // Component is an abstract struct to provide basic components behaviour. 4 | // Components themselves are a singleton layout elements which have a persisted location between app startups. 5 | type Component struct { 6 | visible bool 7 | focused bool 8 | 9 | onVisible []func(bool) 10 | onFocused []func(bool) 11 | } 12 | 13 | func (Component) PreProcess() { 14 | // do nothing 15 | } 16 | 17 | func (Component) Process(int32) { 18 | // do nothing 19 | } 20 | 21 | func (Component) PostProcess() { 22 | // do nothing 23 | } 24 | 25 | func (c Component) Visible() bool { 26 | return c.visible 27 | } 28 | 29 | func (c *Component) SetVisible(visible bool) { 30 | if c.visible != visible { 31 | c.visible = visible 32 | for _, listener := range c.onVisible { 33 | listener(visible) 34 | } 35 | } 36 | } 37 | 38 | func (c *Component) AddOnVisible(listener func(bool)) { 39 | c.onVisible = append(c.onVisible, listener) 40 | } 41 | 42 | func (c Component) Focused() bool { 43 | return c.focused 44 | } 45 | 46 | func (c *Component) SetFocused(focused bool) { 47 | if c.focused != focused { 48 | c.focused = focused 49 | for _, listener := range c.onFocused { 50 | listener(focused) 51 | } 52 | } 53 | } 54 | 55 | func (c *Component) AddOnFocused(listener func(bool)) { 56 | c.onFocused = append(c.onFocused, listener) 57 | } 58 | -------------------------------------------------------------------------------- /internal/app/ui/cpenvironment/config.go: -------------------------------------------------------------------------------- 1 | package cpenvironment 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | const ( 6 | configName = "cpenvironment" 7 | configVersion = 1 8 | ) 9 | 10 | type cpenvironmentConfig struct { 11 | Version uint 12 | 13 | NodeScale int32 14 | } 15 | 16 | func (cpenvironmentConfig) Name() string { 17 | return configName 18 | } 19 | 20 | func (cpenvironmentConfig) TryMigrate(_ map[string]any) (result map[string]any, migrated bool) { 21 | // do nothing. yet... 22 | return nil, migrated 23 | } 24 | 25 | func (e *Environment) loadConfig() { 26 | e.app.ConfigRegister(&cpenvironmentConfig{ 27 | Version: configVersion, 28 | 29 | NodeScale: 100, 30 | }) 31 | } 32 | 33 | func (e *Environment) config() *cpenvironmentConfig { 34 | if cfg, ok := e.app.ConfigFind(configName).(*cpenvironmentConfig); ok { 35 | return cfg 36 | } 37 | log.Fatal().Msg("can't find config") 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/app/ui/cpenvironment/node.go: -------------------------------------------------------------------------------- 1 | package cpenvironment 2 | 3 | import ( 4 | "strings" 5 | 6 | "sdmm/internal/dmapi/dmenv" 7 | "sdmm/internal/dmapi/dmicon" 8 | "sdmm/internal/util" 9 | 10 | "github.com/SpaiR/imgui-go" 11 | ) 12 | 13 | type treeNode struct { 14 | name string 15 | orig *dmenv.Object 16 | sprite *dmicon.Sprite 17 | color imgui.Vec4 18 | dir int 19 | } 20 | 21 | func (e *Environment) newTreeNode(object *dmenv.Object) (*treeNode, bool) { 22 | if node, ok := e.treeNodes[object.Path]; ok { 23 | return node, true 24 | } 25 | 26 | if e.tmpNewTreeNodesCount >= newTreeNodesLimit { 27 | return nil, false 28 | } 29 | 30 | e.tmpNewTreeNodesCount += 1 31 | 32 | icon, _ := object.Vars.Text("icon") 33 | iconState, _ := object.Vars.Text("icon_state") 34 | color := imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1} 35 | dir, _ := object.Vars.Int("dir") 36 | 37 | if col, _ := object.Vars.Text("color"); col != "" { 38 | r, g, b, _ := util.ParseColor(col).RGBA() 39 | color = imgui.Vec4{X: r, Y: g, Z: b, W: 1} 40 | } 41 | 42 | node := &treeNode{ 43 | name: object.Path[strings.LastIndex(object.Path, "/")+1:], 44 | orig: object, 45 | sprite: dmicon.Cache.GetSpriteOrPlaceholderV(icon, iconState, dir), 46 | color: color, 47 | dir: dir, 48 | } 49 | 50 | e.treeNodes[object.Path] = node 51 | return node, true 52 | } 53 | -------------------------------------------------------------------------------- /internal/app/ui/cpenvironment/shortcut.go: -------------------------------------------------------------------------------- 1 | package cpenvironment 2 | 3 | import ( 4 | "sdmm/internal/app/ui/shortcut" 5 | 6 | "github.com/go-gl/glfw/v3.3/glfw" 7 | ) 8 | 9 | func (e *Environment) addShortcuts() { 10 | e.shortcuts.Add(shortcut.Shortcut{ 11 | Name: "cpenvironment#doToggleTypesFilter", 12 | FirstKey: glfw.KeyF, 13 | Action: e.doToggleTypesFilter, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /internal/app/ui/cpprefabs/node.go: -------------------------------------------------------------------------------- 1 | package cpprefabs 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "sdmm/internal/dmapi/dm" 8 | "sdmm/internal/dmapi/dmicon" 9 | "sdmm/internal/dmapi/dmmap" 10 | "sdmm/internal/dmapi/dmmap/dmmdata" 11 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 12 | "sdmm/internal/dmapi/dmvars" 13 | "sdmm/internal/util" 14 | 15 | "github.com/SpaiR/imgui-go" 16 | ) 17 | 18 | type prefabNode struct { 19 | name string 20 | orig *dmmprefab.Prefab 21 | sprite *dmicon.Sprite 22 | color imgui.Vec4 23 | visHeight float32 24 | } 25 | 26 | func newPrefabNodes(prefabs dmmdata.Prefabs) []*prefabNode { 27 | nodes := make([]*prefabNode, 0, len(prefabs)) 28 | for _, prefab := range prefabs { 29 | nodes = append(nodes, newPrefabNode(prefab)) 30 | } 31 | 32 | if nodes != nil { 33 | // Group by icon_state 34 | sort.Slice(nodes, func(i, j int) bool { 35 | iIconState, _ := nodes[i].orig.Vars().Text("icon_state") 36 | jIconState, _ := nodes[j].orig.Vars().Text("icon_state") 37 | return strings.Compare(iIconState, jIconState) == -1 38 | }) 39 | // Group by name 40 | sort.Slice(nodes, func(i, j int) bool { 41 | return strings.Compare(nodes[i].name, nodes[j].name) == -1 42 | }) 43 | 44 | // Find the initial prefab index. 45 | idx := -1 46 | for i, node := range nodes { 47 | if node.orig.Vars().Len() == 0 { 48 | idx = i 49 | break 50 | } 51 | } 52 | 53 | if idx == -1 { 54 | // If the initial prefab index is still -1, then we don't have it. We will add the one. 55 | initialPrefab := dmmap.PrefabStorage.Initial(prefabs[0].Path()) 56 | nodes = append([]*prefabNode{newPrefabNode(initialPrefab)}, nodes...) 57 | } else { 58 | // Move the initial prefab to the beginning of the slice 59 | initial := nodes[idx] 60 | nodes = append(nodes[:idx], nodes[idx+1:]...) 61 | nodes = append([]*prefabNode{initial}, nodes...) 62 | } 63 | } 64 | 65 | return nodes 66 | } 67 | 68 | func newPrefabNode(prefab *dmmprefab.Prefab) *prefabNode { 69 | return newPrefabNodeV(prefab, prefab.Vars().TextV("name", dm.PathLast(prefab.Path()))) 70 | } 71 | 72 | func newPrefabNodeV(prefab *dmmprefab.Prefab, name string) *prefabNode { 73 | icon, _ := prefab.Vars().Text("icon") 74 | iconState, _ := prefab.Vars().Text("icon_state") 75 | dir, _ := prefab.Vars().Int("dir") 76 | r, g, b, _ := util.ParseColor(prefab.Vars().TextV("color", dmvars.NullValue)).RGBA() 77 | return &prefabNode{ 78 | name: name, 79 | orig: prefab, 80 | sprite: dmicon.Cache.GetSpriteOrPlaceholderV(icon, iconState, dir), 81 | color: imgui.Vec4{X: r, Y: g, Z: b, W: 1}, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/app/ui/cpprefabs/prefabs.go: -------------------------------------------------------------------------------- 1 | package cpprefabs 2 | 3 | import ( 4 | "sdmm/internal/app/ui/component" 5 | "sdmm/internal/app/ui/cpwsarea/wsmap/pmap/editor" 6 | "sdmm/internal/app/window" 7 | "sdmm/internal/dmapi/dmenv" 8 | 9 | "sdmm/internal/dmapi/dmmap" 10 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 11 | 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | type App interface { 16 | DoSelectPrefab(*dmmprefab.Prefab) 17 | DoEditPrefab(prefab *dmmprefab.Prefab) 18 | DoSearchPrefab(prefabId uint64) 19 | DoSearchPrefabByPath(path string) 20 | HasActiveMap() bool 21 | ShowLayout(name string, focus bool) 22 | CurrentEditor() *editor.Editor 23 | LoadedEnvironment() *dmenv.Dme 24 | } 25 | 26 | type Prefabs struct { 27 | component.Component 28 | 29 | app App 30 | 31 | nodes []*prefabNode 32 | selectedId uint64 33 | 34 | tmpDoScrollToPrefab bool 35 | } 36 | 37 | func (p *Prefabs) Init(app App) { 38 | p.app = app 39 | } 40 | 41 | func (p *Prefabs) Free() { 42 | p.nodes = nil 43 | p.selectedId = dmmprefab.IdNone 44 | } 45 | 46 | func (p *Prefabs) Select(prefab *dmmprefab.Prefab) { 47 | p.nodes = newPrefabNodes(dmmap.PrefabStorage.GetAllByPath(prefab.Path())) 48 | 49 | // A special case for a "staged" prefab. 50 | if prefab.Id() == dmmprefab.IdStage { 51 | p.nodes = append(p.nodes, newPrefabNodeV(prefab, "[STAGED]")) 52 | } 53 | 54 | p.selectedId = prefab.Id() 55 | p.tmpDoScrollToPrefab = true 56 | log.Print("selected prefab id:", p.selectedId) 57 | } 58 | 59 | func (p *Prefabs) Sync() { 60 | if p.selectedId != dmmprefab.IdNone { 61 | if prefab, ok := dmmap.PrefabStorage.GetById(p.selectedId); ok { 62 | p.Select(prefab) 63 | } 64 | } 65 | } 66 | 67 | // SelectedPrefabId returns the id of the prefab currently selected in the Prefabs panel. 68 | func (p *Prefabs) SelectedPrefabId() uint64 { 69 | return p.selectedId 70 | } 71 | 72 | func (p *Prefabs) doSelect(node *prefabNode) { 73 | p.app.DoSelectPrefab(node.orig) 74 | p.app.DoEditPrefab(node.orig) 75 | p.tmpDoScrollToPrefab = false // do not scroll panel when we're in panel itself 76 | } 77 | 78 | func (p *Prefabs) iconSize() float32 { 79 | return 32 * window.PointSize() 80 | } 81 | -------------------------------------------------------------------------------- /internal/app/ui/cpprefabs/process.go: -------------------------------------------------------------------------------- 1 | package cpprefabs 2 | 3 | import ( 4 | "fmt" 5 | 6 | w "sdmm/internal/imguiext/widget" 7 | 8 | "github.com/SpaiR/imgui-go" 9 | ) 10 | 11 | func (p *Prefabs) Process(int32) { 12 | if len(p.nodes) == 0 { 13 | imgui.TextDisabled("No prefab selected") 14 | return 15 | } 16 | 17 | for _, node := range p.nodes { 18 | isSelected := node.orig.Id() == p.selectedId 19 | cursor := imgui.CursorPos() 20 | 21 | if isSelected && p.tmpDoScrollToPrefab { 22 | imgui.SetScrollHereY(.5) 23 | p.tmpDoScrollToPrefab = false 24 | } 25 | 26 | if node.visHeight != 0 { 27 | if imgui.SelectableV( 28 | fmt.Sprintf("##prefab_%d", node.orig.Id()), 29 | isSelected, 30 | imgui.SelectableFlagsNone, 31 | imgui.Vec2{Y: node.visHeight}, 32 | ) { 33 | p.doSelect(node) 34 | } 35 | 36 | p.showContextMenu(node) 37 | } 38 | 39 | imgui.SetCursorPos(cursor) 40 | 41 | imgui.BeginGroup() 42 | w.Image(imgui.TextureID(node.sprite.Texture()), p.iconSize(), p.iconSize()). 43 | Uv( 44 | imgui.Vec2{ 45 | X: node.sprite.U1, 46 | Y: node.sprite.V1, 47 | }, 48 | imgui.Vec2{ 49 | X: node.sprite.U2, 50 | Y: node.sprite.V2, 51 | }, 52 | ). 53 | TintColor(node.color). 54 | Build() 55 | 56 | imgui.SameLine() 57 | 58 | imgui.BeginGroup() 59 | imgui.Text(node.name) 60 | imgui.EndGroup() 61 | 62 | imgui.EndGroup() 63 | 64 | node.visHeight = imgui.ItemRectMax().Y - imgui.ItemRectMin().Y 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/app/ui/cpsearch/search.go: -------------------------------------------------------------------------------- 1 | package cpsearch 2 | 3 | import ( 4 | "strconv" 5 | 6 | "sdmm/internal/app/ui/component" 7 | "sdmm/internal/app/ui/cpwsarea/wsmap/pmap/editor" 8 | 9 | "sdmm/internal/app/ui/shortcut" 10 | "sdmm/internal/dmapi/dmmap/dmminstance" 11 | "sdmm/internal/util" 12 | 13 | "github.com/rs/zerolog/log" 14 | ) 15 | 16 | type App interface { 17 | CurrentEditor() *editor.Editor 18 | DoEditInstance(*dmminstance.Instance) 19 | ShowLayout(name string, focus bool) 20 | } 21 | 22 | type Search struct { 23 | component.Component 24 | 25 | app App 26 | 27 | shortcuts shortcut.Shortcuts 28 | 29 | prefabId string 30 | 31 | selectedResultIdx int 32 | focusedResultIdx int 33 | lastFocusedResultIdx int 34 | 35 | filterActive bool 36 | filterBound util.Bounds 37 | 38 | resultsAll []*dmminstance.Instance 39 | resultsFiltered []*dmminstance.Instance 40 | } 41 | 42 | func (s *Search) Init(app App) { 43 | s.app = app 44 | 45 | s.selectedResultIdx = -1 46 | s.focusedResultIdx = -1 47 | s.lastFocusedResultIdx = -1 48 | 49 | s.addShortcuts() 50 | 51 | s.AddOnFocused(func(focused bool) { 52 | s.shortcuts.SetVisible(focused) 53 | }) 54 | } 55 | 56 | func (s *Search) Free() { 57 | s.resultsAll = s.resultsAll[:0] 58 | s.selectedResultIdx = -1 59 | s.focusedResultIdx = -1 60 | s.lastFocusedResultIdx = -1 61 | s.doResetFilter() 62 | log.Print("search free") 63 | } 64 | 65 | func (s *Search) Sync() { 66 | s.doSearch() 67 | } 68 | 69 | func (s *Search) Search(prefabId uint64) { 70 | s.prefabId = strconv.FormatUint(prefabId, 10) 71 | s.doSearch() 72 | } 73 | 74 | func (s *Search) SearchByPath(path string) { 75 | s.prefabId = path 76 | s.doSearch() 77 | } 78 | 79 | func (s *Search) results() []*dmminstance.Instance { 80 | if !s.filterBound.IsEmpty() { 81 | return s.resultsFiltered 82 | } 83 | return s.resultsAll 84 | } 85 | -------------------------------------------------------------------------------- /internal/app/ui/cpsearch/shortcut.go: -------------------------------------------------------------------------------- 1 | package cpsearch 2 | 3 | import ( 4 | "sdmm/internal/app/ui/shortcut" 5 | 6 | "github.com/go-gl/glfw/v3.3/glfw" 7 | ) 8 | 9 | func (s *Search) addShortcuts() { 10 | s.shortcuts.Add(shortcut.Shortcut{ 11 | Name: "cpsearch#jumpToUp", 12 | FirstKey: glfw.KeyLeftShift, 13 | FirstKeyAlt: glfw.KeyRightShift, 14 | SecondKey: glfw.KeyF3, 15 | Action: s.jumpToUp, 16 | }) 17 | s.shortcuts.Add(shortcut.Shortcut{ 18 | Name: "cpsearch#jumpToDown", 19 | FirstKey: glfw.KeyF3, 20 | Action: s.jumpToDown, 21 | }) 22 | s.shortcuts.Add(shortcut.Shortcut{ 23 | Name: "cpseaarch#doToggleFilter", 24 | FirstKey: glfw.KeyF, 25 | Action: s.doToggleFilter, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/ui/cpvareditor/collect.go: -------------------------------------------------------------------------------- 1 | package cpvareditor 2 | 3 | import ( 4 | "sort" 5 | 6 | "sdmm/internal/dmapi/dmenv" 7 | "sdmm/internal/dmapi/dmvars" 8 | "sdmm/internal/util/slice" 9 | ) 10 | 11 | var unmodifiableVars = []string{ 12 | "type", "parent_type", "vars", "x", "y", "z", "filters", 13 | "loc", "maptext", "maptext_width", "maptext_height", "maptext_x", "maptext_y", 14 | "overlays", "underlays", "verbs", "appearance", "vis_locs", "vis_contents", 15 | "vis_flags", "bounds", "particles", "render_source", "render_target", 16 | } 17 | 18 | func collectVariablesNames(vars *dmvars.Variables) (variablesNames []string) { 19 | variablesNames = collectVariablesNames0(vars) 20 | sort.Strings(variablesNames) 21 | return variablesNames 22 | } 23 | 24 | func collectVariablesNames0(vars *dmvars.Variables) []string { 25 | variablesNames := make([]string, 0, vars.Len()) 26 | for _, varName := range vars.Iterate() { 27 | if !slice.StrContains(unmodifiableVars, varName) { 28 | variablesNames = append(variablesNames, varName) 29 | } 30 | } 31 | if vars.HasParent() { 32 | for _, parentVarName := range collectVariablesNames0(vars.Parent()) { 33 | variablesNames = slice.StrPushUnique(variablesNames, parentVarName) 34 | } 35 | } 36 | return variablesNames 37 | } 38 | 39 | func collectVariablesPaths(obj *dmenv.Object) (variablesPaths []string) { 40 | for { 41 | variablesPaths = append(variablesPaths, obj.Path) 42 | obj = obj.Parent() 43 | if obj == nil { 44 | break 45 | } 46 | } 47 | return variablesPaths 48 | } 49 | 50 | func collectVariablesNamesByPaths(dme *dmenv.Dme, variablesPaths []string) map[string][]string { 51 | variablesNamesByPaths := make(map[string][]string) 52 | 53 | for _, path := range variablesPaths { 54 | obj := dme.Objects[path] 55 | variablesNames := make([]string, 0, len(obj.Vars.Iterate())) 56 | 57 | for _, varName := range obj.Vars.Iterate() { 58 | if parent := obj.Parent(); parent != nil && isParentObjsHasVar0(parent, varName) { 59 | continue 60 | } 61 | if slice.StrContains(unmodifiableVars, varName) { 62 | continue 63 | } 64 | variablesNames = append(variablesNames, varName) 65 | } 66 | 67 | sort.Strings(variablesNames) 68 | variablesNamesByPaths[path] = variablesNames 69 | } 70 | 71 | return variablesNamesByPaths 72 | } 73 | 74 | func isParentObjsHasVar0(obj *dmenv.Object, varName string) bool { 75 | if slice.StrContains(obj.Vars.Iterate(), varName) { 76 | return true 77 | } 78 | if parent := obj.Parent(); parent != nil { 79 | return isParentObjsHasVar0(parent, varName) 80 | } 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /internal/app/ui/cpvareditor/config.go: -------------------------------------------------------------------------------- 1 | package cpvareditor 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | const ( 6 | configName = "cpvareditor" 7 | configVersion = 1 8 | ) 9 | 10 | type vareditorConfig struct { 11 | Version uint 12 | 13 | ShowModified bool 14 | ShowByType bool 15 | ShowPins bool 16 | ShowTmp bool 17 | 18 | PinnedVarNames []string 19 | } 20 | 21 | func (vareditorConfig) Name() string { 22 | return configName 23 | } 24 | 25 | func (vareditorConfig) TryMigrate(_ map[string]any) (result map[string]any, migrated bool) { 26 | // do nothing. yet... 27 | return nil, migrated 28 | } 29 | 30 | func (v *VarEditor) loadConfig() { 31 | v.app.ConfigRegister(&vareditorConfig{ 32 | Version: configVersion, 33 | 34 | ShowPins: true, 35 | }) 36 | } 37 | 38 | func (v *VarEditor) config() *vareditorConfig { 39 | if cfg, ok := v.app.ConfigFind(configName).(*vareditorConfig); ok { 40 | return cfg 41 | } 42 | log.Fatal().Msg("can't find config") 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/ui/cpvareditor/shortcut.go: -------------------------------------------------------------------------------- 1 | package cpvareditor 2 | 3 | import ( 4 | "sdmm/internal/app/ui/shortcut" 5 | "sdmm/internal/platform" 6 | 7 | "github.com/go-gl/glfw/v3.3/glfw" 8 | ) 9 | 10 | func (v *VarEditor) addShortcuts() { 11 | v.shortcuts.Add(shortcut.Shortcut{ 12 | Name: "cpvareditor#doToggleShowModified", 13 | FirstKey: platform.KeyModLeft(), 14 | FirstKeyAlt: platform.KeyModRight(), 15 | SecondKey: glfw.Key1, 16 | SecondKeyAlt: glfw.KeyKP1, 17 | Action: v.doToggleShowModified, 18 | }) 19 | v.shortcuts.Add(shortcut.Shortcut{ 20 | Name: "cpvareditor#doToggleShowByType", 21 | FirstKey: platform.KeyModLeft(), 22 | FirstKeyAlt: platform.KeyModRight(), 23 | SecondKey: glfw.Key2, 24 | SecondKeyAlt: glfw.KeyKP2, 25 | Action: v.doToggleShowByType, 26 | }) 27 | v.shortcuts.Add(shortcut.Shortcut{ 28 | Name: "cpvareditor#doToggleShowPins", 29 | FirstKey: platform.KeyModLeft(), 30 | FirstKeyAlt: platform.KeyModRight(), 31 | SecondKey: glfw.Key3, 32 | SecondKeyAlt: glfw.KeyKP3, 33 | Action: v.doToggleShowPins, 34 | }) 35 | v.shortcuts.Add(shortcut.Shortcut{ 36 | Name: "cpvareditor#doToggleShowTmp", 37 | FirstKey: platform.KeyModLeft(), 38 | FirstKeyAlt: platform.KeyModRight(), 39 | SecondKey: glfw.Key4, 40 | SecondKeyAlt: glfw.KeyKP4, 41 | Action: v.doToggleShowTmp, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/config.go: -------------------------------------------------------------------------------- 1 | package cpwsarea 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | const ( 6 | configName = "cpwsarea" 7 | configVersion = 1 8 | ) 9 | 10 | type cpwsareaConfig struct { 11 | Version uint 12 | 13 | LastChangelogHash uint64 14 | } 15 | 16 | func (cpwsareaConfig) Name() string { 17 | return configName 18 | } 19 | 20 | func (cpwsareaConfig) TryMigrate(_ map[string]any) (result map[string]any, migrated bool) { 21 | // do nothing. yet... 22 | return nil, migrated 23 | } 24 | 25 | func (w *WsArea) loadConfig() { 26 | w.app.ConfigRegister(&cpwsareaConfig{ 27 | Version: configVersion, 28 | }) 29 | } 30 | 31 | func (w *WsArea) config() *cpwsareaConfig { 32 | if cfg, ok := w.app.ConfigFind(configName).(*cpwsareaConfig); ok { 33 | return cfg 34 | } 35 | log.Fatal().Msg("can't find config") 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/logo.go: -------------------------------------------------------------------------------- 1 | package cpwsarea 2 | 3 | import ( 4 | "image/color" 5 | 6 | "sdmm/internal/app/window" 7 | 8 | "github.com/SpaiR/imgui-go" 9 | ) 10 | 11 | const logoSize = 250 12 | 13 | var ( 14 | logoColor = imgui.Packed(color.RGBA{R: 200, G: 200, B: 200, A: 75}) 15 | logoColorHovered = imgui.Packed(color.RGBA{R: 200, G: 200, B: 200, A: 125}) 16 | ) 17 | 18 | // Show application logo in the center of the window. 19 | func (w *WsArea) showAppLogo(dockId int) { 20 | imgui.SetNextWindowDockIDV(dockId, imgui.ConditionAlways) 21 | imgui.ExtSetNextWindowDockNodeFlags(imgui.DockNodeFlagsNoTabBar) 22 | if imgui.BeginV("workspace_area_help", nil, imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsNoDecoration) { 23 | winSize := imgui.WindowSize() 24 | winPos := imgui.WindowPos() 25 | 26 | size := imgui.Vec2{X: logoSize * window.PointSize(), Y: logoSize * window.PointSize()} 27 | pos := winPos.Plus(winSize.Minus(size).Times(.5)) 28 | 29 | imgui.SetCursorScreenPos(pos) 30 | imgui.Dummy(size) 31 | 32 | var aColor imgui.PackedColor 33 | if imgui.IsItemHovered() { 34 | imgui.SetMouseCursor(imgui.MouseCursorHand) 35 | aColor = logoColorHovered 36 | } else { 37 | aColor = logoColor 38 | } 39 | 40 | if imgui.IsItemClicked() { 41 | w.AddEmptyWorkspace() 42 | } 43 | 44 | imgui.WindowDrawList().AddImageV( 45 | imgui.TextureID(window.AppLogoTexture), 46 | pos, 47 | pos.Plus(size), 48 | imgui.Vec2{}, 49 | imgui.Vec2{X: 1, Y: 1}, 50 | aColor, 51 | ) 52 | } 53 | imgui.End() 54 | } 55 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/process.go: -------------------------------------------------------------------------------- 1 | package cpwsarea 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/workspace" 5 | 6 | "github.com/SpaiR/imgui-go" 7 | ) 8 | 9 | var tmpFocusedWs *workspace.Workspace 10 | 11 | func (w *WsArea) Process(dockId int32) { 12 | if len(w.workspaces) == 0 { 13 | w.switchActiveWorkspace(nil) 14 | w.showAppLogo(int(dockId)) 15 | } 16 | 17 | w.processWorkspaces(int(dockId)) 18 | 19 | w.switchFocusedWorkspace(tmpFocusedWs) 20 | tmpFocusedWs = nil 21 | } 22 | 23 | func (w *WsArea) processWorkspaces(dockId int) { 24 | var workspacesToClose []*workspace.Workspace 25 | for _, ws := range w.workspaces { 26 | ws.PreProcess() 27 | 28 | // When the window of the workspace is closed we need to dispose its content as well. 29 | if !w.showWorkspaceWindow(dockId, ws) { 30 | workspacesToClose = append(workspacesToClose, ws) 31 | } 32 | 33 | ws.PostProcess() 34 | } 35 | 36 | for _, ws := range workspacesToClose { 37 | w.closeWorkspaceGently(ws) 38 | } 39 | } 40 | 41 | func (w *WsArea) showWorkspaceWindow(dockId int, ws *workspace.Workspace) (open bool) { 42 | open = true 43 | id := ws.Name() + "###" + ws.Id() 44 | 45 | dockCondition := imgui.ConditionOnce 46 | if w.app.IsLayoutReset() { 47 | dockCondition = imgui.ConditionAlways 48 | } 49 | 50 | imgui.SetNextWindowDockIDV(dockId, dockCondition) 51 | 52 | flags := imgui.WindowFlagsNoSavedSettings | ws.Content().Ini().WindowFlags 53 | 54 | if ws.Content().Ini().NoPadding { 55 | imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{}) 56 | } 57 | 58 | visible := imgui.BeginV(id, &open, flags) 59 | 60 | if visible { 61 | if ws.Content().Ini().NoPadding { 62 | imgui.PopStyleVar() 63 | } 64 | 65 | ws.Process() 66 | } else if ws.Content().Ini().NoPadding { 67 | imgui.PopStyleVar() 68 | } 69 | 70 | if ws.Focused() { 71 | w.switchActiveWorkspace(ws) 72 | tmpFocusedWs = ws 73 | } else if ws.TriggerFocus() { 74 | ws.SetTriggerFocus(false) 75 | imgui.SetWindowFocus() 76 | } 77 | 78 | imgui.End() 79 | 80 | return open || ws.Closed() 81 | } 82 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/workspace/content.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "sdmm/internal/app/command" 8 | 9 | "github.com/SpaiR/imgui-go" 10 | ) 11 | 12 | type content interface { 13 | SetRoot(*Workspace) // Root will be installed by the Workspace when it's created. 14 | Root() *Workspace 15 | 16 | Id() string 17 | Name() string 18 | Title() string 19 | 20 | Focused() bool 21 | Closed() bool 22 | OnFocusChange(focused bool) 23 | 24 | Initialize() 25 | PreProcess() 26 | Process() 27 | PostProcess() 28 | Dispose() 29 | 30 | Save() bool 31 | CommandStackId() string 32 | Ini() Ini 33 | } 34 | 35 | type Content struct { 36 | root *Workspace 37 | 38 | id string 39 | 40 | closed bool 41 | } 42 | 43 | func (c *Content) SetRoot(root *Workspace) { 44 | c.root = root 45 | } 46 | 47 | func (c *Content) Root() *Workspace { 48 | return c.root 49 | } 50 | 51 | func (c *Content) Close() { 52 | c.closed = true 53 | } 54 | 55 | var contentCount uint64 56 | 57 | func (c *Content) Id() string { 58 | if c.id == "" { 59 | c.id = fmt.Sprint("content_", time.Now().Nanosecond(), "_", contentCount) 60 | contentCount++ 61 | } 62 | return c.id 63 | } 64 | 65 | func (Content) Focused() bool { 66 | return imgui.IsWindowFocusedV(imgui.FocusedFlagsRootAndChildWindows) 67 | } 68 | 69 | func (c *Content) Closed() bool { 70 | return c.closed 71 | } 72 | 73 | func (Content) OnFocusChange(bool) { 74 | // do nothing 75 | } 76 | 77 | func (Content) Initialize() { 78 | // do nothing 79 | } 80 | 81 | func (Content) PreProcess() { 82 | // do nothing 83 | } 84 | 85 | func (Content) Process() { 86 | // do nothing 87 | } 88 | 89 | func (Content) PostProcess() { 90 | // do nothing 91 | } 92 | 93 | func (Content) Dispose() { 94 | // do nothing 95 | } 96 | 97 | func (Content) Save() bool { 98 | return false 99 | } 100 | 101 | func (Content) CommandStackId() string { 102 | return command.NullSpaceStackId 103 | } 104 | 105 | func (Content) Ini() Ini { 106 | return Ini{} 107 | } 108 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/workspace/ini.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type Ini struct { 6 | WindowFlags imgui.WindowFlags 7 | NoPadding bool 8 | } 9 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/workspace/workspace.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Workspace struct { 9 | id string 10 | 11 | triggerFocus bool 12 | 13 | content content 14 | } 15 | 16 | func (ws *Workspace) TriggerFocus() bool { 17 | return ws.triggerFocus 18 | } 19 | 20 | func (ws *Workspace) SetTriggerFocus(triggerFocus bool) { 21 | ws.triggerFocus = triggerFocus 22 | } 23 | 24 | //goland:noinspection GoExportedFuncWithUnexportedType 25 | func (ws *Workspace) Content() content { 26 | return ws.content 27 | } 28 | 29 | func (ws *Workspace) SetContent(cnt content) { 30 | if ws.content != nil { 31 | ws.content.Dispose() 32 | } 33 | ws.content = cnt 34 | cnt.SetRoot(ws) 35 | } 36 | 37 | func New(cnt content) *Workspace { 38 | ws := &Workspace{content: cnt} 39 | cnt.SetRoot(ws) 40 | return ws 41 | } 42 | 43 | func (ws *Workspace) String() string { 44 | return ws.Id() 45 | } 46 | 47 | func (ws *Workspace) Name() string { 48 | return ws.content.Name() 49 | } 50 | 51 | func (ws *Workspace) Title() string { 52 | return ws.content.Title() 53 | } 54 | 55 | var workspaceCount uint64 56 | 57 | func (ws *Workspace) Id() string { 58 | if ws.id == "" { 59 | ws.id = fmt.Sprint("workspace_", time.Now().Nanosecond(), "_", workspaceCount) 60 | workspaceCount++ 61 | } 62 | return ws.id 63 | } 64 | 65 | func (ws *Workspace) CommandStackId() string { 66 | return ws.content.CommandStackId() 67 | } 68 | 69 | func (ws *Workspace) OnFocusChange(focused bool) { 70 | ws.content.OnFocusChange(focused) 71 | } 72 | 73 | func (ws *Workspace) Initialize() { 74 | ws.content.Initialize() 75 | } 76 | 77 | func (ws *Workspace) PreProcess() { 78 | ws.content.PreProcess() 79 | } 80 | 81 | func (ws *Workspace) Process() { 82 | ws.content.Process() 83 | } 84 | 85 | func (ws *Workspace) PostProcess() { 86 | ws.content.PostProcess() 87 | } 88 | 89 | func (ws *Workspace) Dispose() { 90 | ws.content.Dispose() 91 | } 92 | 93 | func (ws *Workspace) Focused() bool { 94 | return ws.content.Focused() 95 | } 96 | 97 | func (ws *Workspace) Closed() bool { 98 | return ws.content.Closed() 99 | } 100 | 101 | func (ws *Workspace) Save() bool { 102 | return ws.Content().Save() 103 | } 104 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wschangelog/wschangelog.go: -------------------------------------------------------------------------------- 1 | package wschangelog 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/workspace" 5 | "sdmm/internal/app/window" 6 | "sdmm/internal/imguiext/icon" 7 | "sdmm/internal/imguiext/markdown" 8 | "sdmm/internal/imguiext/style" 9 | w "sdmm/internal/imguiext/widget" 10 | "sdmm/internal/rsc" 11 | 12 | "github.com/SpaiR/imgui-go" 13 | ) 14 | 15 | var ( 16 | parsedChangelog markdown.Markdown 17 | ) 18 | 19 | type App interface { 20 | DoOpenSourceCode() 21 | DoOpenSupport() 22 | } 23 | 24 | type WsChangelog struct { 25 | workspace.Content 26 | 27 | app App 28 | } 29 | 30 | func New(app App) *WsChangelog { 31 | return &WsChangelog{ 32 | app: app, 33 | } 34 | } 35 | 36 | func (ws *WsChangelog) Name() string { 37 | return icon.ClipboardMultiple + " Changelog" 38 | } 39 | 40 | func (ws *WsChangelog) Title() string { 41 | return "Changelog" 42 | } 43 | 44 | func (ws *WsChangelog) Process() { 45 | ws.showContent() 46 | } 47 | 48 | func (ws *WsChangelog) showContent() { 49 | if parsedChangelog.IsEmpty() { 50 | parsedChangelog = markdown.Parse(rsc.ChangelogMd) 51 | } 52 | 53 | logoSize := 100 * window.PointSize() 54 | 55 | w.Layout{ 56 | w.Image(imgui.TextureID(window.AppLogoTexture), logoSize, logoSize), 57 | w.SameLine(), 58 | w.Group{ 59 | w.Custom(func() { 60 | markdown.ShowHeader("StrongDMM Changelog", window.FontH3) 61 | }), 62 | w.Separator(), 63 | w.TextWrapped(rsc.ChangelogHeaderTxt), 64 | w.NewLine(), 65 | w.Button("Open Source Code", ws.app.DoOpenSourceCode). 66 | Icon(icon.GitHub), 67 | w.SameLine(), 68 | w.Dummy(imgui.Vec2{}), 69 | w.SameLine(), 70 | w.Button("Support the Project", ws.app.DoOpenSupport). 71 | Style(style.ButtonFireCoral{}). 72 | Tooltip(rsc.SupportTxt). 73 | Icon(icon.KoFi), 74 | }, 75 | }.Build() 76 | 77 | imgui.NewLine() 78 | markdown.Show(parsedChangelog) 79 | } 80 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wscreatemap/save.go: -------------------------------------------------------------------------------- 1 | package wscreatemap 2 | 3 | import ( 4 | "sdmm/internal/app/prefs" 5 | "sdmm/internal/dmapi/dmmap" 6 | "sdmm/internal/dmapi/dmmap/dmmdata" 7 | "sdmm/internal/util" 8 | 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | func (ws *WsCreateMap) save(newPath string) { 13 | log.Print("saving new map:", newPath) 14 | 15 | // we assume the data is OK at this point 16 | var data = &dmmdata.DmmData{ 17 | Filepath: newPath, 18 | IsTgm: ws.format == prefs.SaveFormatTGM, 19 | LineBreak: "\n", 20 | MaxX: ws.mapWidth, 21 | MaxY: ws.mapHeight, 22 | MaxZ: ws.mapZDepth, 23 | Dictionary: make(dmmdata.DataDictionary), 24 | Grid: make(dmmdata.DataGrid), 25 | } 26 | 27 | data.Dictionary["a"] = dmmdata.Prefabs{ 28 | dmmap.BaseArea, 29 | dmmap.BaseTurf, 30 | } 31 | 32 | for z := 1; z <= data.MaxZ; z++ { 33 | for y := 1; y <= data.MaxY; y++ { 34 | for x := 1; x <= data.MaxX; x++ { 35 | data.Grid[util.Point{X: x, Y: y, Z: z}] = "a" 36 | } 37 | } 38 | } 39 | 40 | data.Save() 41 | } 42 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsempty/shortcut.go: -------------------------------------------------------------------------------- 1 | package wsempty 2 | 3 | import ( 4 | "sdmm/internal/app/ui/shortcut" 5 | "sdmm/internal/platform" 6 | 7 | "github.com/go-gl/glfw/v3.3/glfw" 8 | ) 9 | 10 | func (ws *WsEmpty) addShortcuts() { 11 | ws.shortcuts.Add(shortcut.Shortcut{ 12 | Name: "wsempty#loadSelectedMaps", 13 | FirstKey: glfw.KeyEnter, 14 | FirstKeyAlt: glfw.KeyKPEnter, 15 | Action: ws.loadSelectedMaps, 16 | }) 17 | 18 | ws.shortcuts.Add(shortcut.Shortcut{ 19 | Name: "wsempty#dropSelectedMaps", 20 | FirstKey: glfw.KeyEscape, 21 | Action: ws.dropSelectedMaps, 22 | }) 23 | ws.shortcuts.Add(shortcut.Shortcut{ 24 | Name: "wsempty#dropSelectedMaps", 25 | FirstKey: platform.KeyModLeft(), 26 | FirstKeyAlt: platform.KeyModRight(), 27 | SecondKey: glfw.KeyD, 28 | Action: ws.dropSelectedMaps, 29 | }) 30 | 31 | ws.shortcuts.Add(shortcut.Shortcut{ 32 | Name: "wsempty#selectAllMaps", 33 | FirstKey: platform.KeyModLeft(), 34 | FirstKeyAlt: platform.KeyModRight(), 35 | SecondKey: glfw.KeyA, 36 | Action: ws.selectAllMaps, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/canvas/overlay.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import ( 4 | "sdmm/internal/app/render" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | type OverlayArea struct { 9 | Bounds_ util.Bounds 10 | FillColor_ util.Color 11 | BorderColor_ util.Color 12 | } 13 | 14 | func (o OverlayArea) Bounds() util.Bounds { 15 | return o.Bounds_ 16 | } 17 | 18 | func (o OverlayArea) FillColor() util.Color { 19 | return o.FillColor_ 20 | } 21 | 22 | func (o OverlayArea) BorderColor() util.Color { 23 | return o.BorderColor_ 24 | } 25 | 26 | type Overlay struct { 27 | areas []render.OverlayArea 28 | units map[uint64]render.HighlightUnit 29 | areasBorders []render.AreaBorder 30 | } 31 | 32 | func NewOverlay() *Overlay { 33 | return &Overlay{ 34 | units: make(map[uint64]render.HighlightUnit), 35 | } 36 | } 37 | 38 | func (o *Overlay) PushArea(area OverlayArea) { 39 | o.areas = append(o.areas, area) 40 | } 41 | 42 | func (o *Overlay) Areas() []render.OverlayArea { 43 | return o.areas 44 | } 45 | 46 | func (o *Overlay) FlushAreas() { 47 | o.areas = o.areas[:0] 48 | } 49 | 50 | type HighlightUnit struct { 51 | Id_ uint64 52 | Color_ util.Color 53 | } 54 | 55 | func (o *Overlay) PushUnit(unit HighlightUnit) { 56 | o.units[unit.Id()] = unit 57 | } 58 | 59 | func (o *Overlay) Units() map[uint64]render.HighlightUnit { 60 | return o.units 61 | } 62 | 63 | func (o *Overlay) FlushUnits() { 64 | for id := range o.units { 65 | delete(o.units, id) 66 | } 67 | } 68 | 69 | func (h HighlightUnit) Id() uint64 { 70 | return h.Id_ 71 | } 72 | 73 | func (h HighlightUnit) Color() util.Color { 74 | return h.Color_ 75 | } 76 | 77 | type OverlayAreaBorder struct { 78 | Borders_ []util.Bounds 79 | Color_ util.Color 80 | } 81 | 82 | func (o OverlayAreaBorder) Borders() []util.Bounds { 83 | return o.Borders_ 84 | } 85 | 86 | func (o OverlayAreaBorder) Color() util.Color { 87 | return o.Color_ 88 | } 89 | 90 | func (o *Overlay) PushAreaBorder(areaBorder OverlayAreaBorder) { 91 | o.areasBorders = append(o.areasBorders, areaBorder) 92 | } 93 | 94 | func (o *Overlay) AreasBorders() []render.AreaBorder { 95 | return o.areasBorders 96 | } 97 | 98 | func (o *Overlay) FlushAreasBorders() { 99 | o.areasBorders = o.areasBorders[:0] 100 | } 101 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/canvas_camera.go: -------------------------------------------------------------------------------- 1 | package pmap 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmmap" 5 | "sdmm/internal/imguiext" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | "github.com/go-gl/glfw/v3.3/glfw" 9 | ) 10 | 11 | const scaleFactor float32 = 1.5 12 | 13 | func (p *PaneMap) processCanvasCamera() { 14 | p.processCameraMove() 15 | p.processCameraZoom() 16 | } 17 | 18 | func (p *PaneMap) processCameraMove() { 19 | if p.canvasControl.Moving() { 20 | if delta := imgui.CurrentIO().MouseDelta(); delta.X != 0 || delta.Y != 0 { 21 | p.translateCanvas(delta.X, delta.Y) 22 | } 23 | } 24 | } 25 | 26 | func (p *PaneMap) processCameraZoom() { 27 | if !p.canvasControl.Zoomed() || !p.canvasControl.Active() { 28 | return 29 | } 30 | 31 | camera := p.canvas.Render().Camera 32 | _, mouseWheel := imgui.CurrentIO().MouseWheel() 33 | 34 | // Support for alternative scroll behaviour. 35 | // Pan with a scroll, zoom if a space key pressed. 36 | if p.app.Prefs().Controls.AltScrollBehaviour && !imgui.IsKeyDown(int(glfw.KeySpace)) { 37 | shift := p.calcManualCanvasTranslateShiftV(mouseWheel) 38 | if imguiext.IsCtrlDown() { 39 | p.translateCanvas(shift, 0) 40 | } else { 41 | p.translateCanvas(0, shift) 42 | } 43 | return 44 | } 45 | 46 | zoomIn := mouseWheel > 0 47 | scale := camera.Scale 48 | 49 | if zoomIn { 50 | scale *= -scaleFactor 51 | } 52 | 53 | mousePos := imgui.MousePos() 54 | localPos := mousePos.Minus(p.canvasControl.PosMin()) 55 | 56 | offsetX := localPos.X / scale / 2 57 | offsetY := (p.size.Y - localPos.Y) / scale / 2 58 | 59 | camera.Translate(offsetX, offsetY) 60 | camera.Zoom(zoomIn, scaleFactor) 61 | } 62 | 63 | func (p *PaneMap) calcManualCanvasTranslateShift() float32 { 64 | return p.calcManualCanvasTranslateShiftV(1) 65 | } 66 | 67 | func (p *PaneMap) calcManualCanvasTranslateShiftV(mod float32) float32 { 68 | value := mod * float32(dmmap.WorldIconSize) 69 | if imguiext.IsShiftDown() { 70 | return value * 5 71 | } 72 | return value 73 | } 74 | 75 | func (p *PaneMap) translateCanvas(shiftX, shiftY float32) { 76 | camera := p.canvas.Render().Camera 77 | camera.Translate(shiftX/camera.Scale, -shiftY/camera.Scale) 78 | } 79 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/canvas_control.go: -------------------------------------------------------------------------------- 1 | package pmap 2 | 3 | func (p *PaneMap) updateCanvasMousePosition(mouseX, mouseY int) { 4 | // If canvas itself is not active, then no need to search for mouse position at all. 5 | if !p.canvasControl.Active() { 6 | p.canvasState.SetMousePosition(-1, -1, -1) 7 | return 8 | } 9 | 10 | // Mouse position relative to canvas. 11 | relMouseX := float32(mouseX - int(p.canvasControl.PosMin().X)) 12 | relMouseY := float32(mouseY - int(p.canvasControl.PosMin().Y)) 13 | 14 | // Canvas height itself. 15 | canvasHeight := p.canvasControl.PosMax().Y - p.canvasControl.PosMin().Y 16 | 17 | // Mouse position by Y axis, but with bottom-up orientation. 18 | relMouseY = canvasHeight - relMouseY 19 | 20 | // Transformed coordinates with respect of camera scale and shift. 21 | camera := p.canvas.Render().Camera 22 | relMouseX = relMouseX/camera.Scale - (camera.ShiftX) 23 | relMouseY = relMouseY/camera.Scale - (camera.ShiftY) 24 | 25 | p.canvasState.SetMousePosition(int(relMouseX), int(relMouseY), p.activeLevel) 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/editor/commit.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "sdmm/internal/app/command" 5 | "sdmm/internal/app/window" 6 | "sdmm/internal/util" 7 | ) 8 | 9 | func (e *Editor) CommitMapSizeChange(oldMaxX, oldMaxY, oldMaxZ int) { 10 | initialMapTiles := e.pMap.Snapshot().Initial().Copy().Tiles // Remember initial tiles to restore them on undo. 11 | newMaxX, newMaxY, newMaxZ := e.dmm.MaxX, e.dmm.MaxY, e.dmm.MaxZ 12 | 13 | e.onMapSizeChange(e.dmm.MaxZ) 14 | 15 | e.app.CommandStorage().Push(command.Make("Set Map Size", func() { 16 | e.dmm.SetMapSize(oldMaxX, oldMaxY, oldMaxZ) 17 | e.dmm.Tiles = initialMapTiles 18 | e.onMapSizeChange(oldMaxZ) 19 | }, func() { 20 | e.dmm.SetMapSize(newMaxX, newMaxY, newMaxZ) 21 | e.onMapSizeChange(newMaxZ) 22 | })) 23 | } 24 | 25 | func (e *Editor) onMapSizeChange(maxZ int) { 26 | // Ensure we are on the visible level. 27 | if e.pMap.ActiveLevel() > maxZ { 28 | e.pMap.SetActiveLevel(maxZ) 29 | } 30 | e.pMap.Snapshot().Sync() // Do a full snapshots sync. 31 | e.pMap.OnMapSizeChange() 32 | e.updateAreasZones() 33 | } 34 | 35 | // CommitChanges triggers a snapshot to commit changes and create a patch between two map states. 36 | func (e *Editor) CommitChanges(commitMsg string) { 37 | go e.commitChanges(commitMsg) 38 | } 39 | 40 | // Used as a wrapper to do a stuff inside the goroutine. 41 | func (e *Editor) commitChanges(commitMsg string) { 42 | stateId, tilesToUpdate := e.pMap.Snapshot().Commit() 43 | 44 | // Do not push command if there is no tiles to update. 45 | if len(tilesToUpdate) == 0 { 46 | return 47 | } 48 | 49 | // Copy the value to pass it to the lambda. 50 | activeLevel := e.pMap.ActiveLevel() 51 | 52 | // Ensure that the user has updated visuals. 53 | e.updateAreasZones() 54 | e.updateBucket(activeLevel, tilesToUpdate) 55 | 56 | e.app.CommandStorage().Push(command.Make(commitMsg, func() { 57 | e.pMap.Snapshot().GoTo(stateId - 1) 58 | e.updateAreasZones() 59 | e.updateBucket(activeLevel, tilesToUpdate) 60 | e.dmm.PersistPrefabs() 61 | e.app.SyncPrefabs() 62 | e.app.SyncVarEditor() 63 | }, func() { 64 | e.pMap.Snapshot().GoTo(stateId) 65 | e.updateAreasZones() 66 | e.updateBucket(activeLevel, tilesToUpdate) 67 | e.dmm.PersistPrefabs() 68 | e.app.SyncPrefabs() 69 | e.app.SyncVarEditor() 70 | })) 71 | } 72 | 73 | // We need to update bucket in the main thread, since it can have OpenGL operations. 74 | // RunLater do that by running the job in th end of the frame. 75 | func (e *Editor) updateBucket(activeLevel int, tilesToUpdate []util.Point) { 76 | window.RunLater(func() { 77 | e.pMap.Canvas().Render().UpdateBucketV(e.dmm, activeLevel, tilesToUpdate) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/editor/overlay.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/wsmap/pmap/overlay" 5 | "sdmm/internal/dmapi/dmmap" 6 | "sdmm/internal/dmapi/dmmap/dmminstance" 7 | "sdmm/internal/util" 8 | 9 | "github.com/SpaiR/imgui-go" 10 | ) 11 | 12 | // OverlayPushTile pushes tile overlay for the next frame. 13 | func (e *Editor) OverlayPushTile(coord util.Point, colFill, colBorder util.Color) { 14 | e.OverlayPushArea(util.Bounds{ 15 | X1: float32(coord.X), 16 | Y1: float32(coord.Y), 17 | X2: float32(coord.X), 18 | Y2: float32(coord.Y), 19 | }, colFill, colBorder) 20 | } 21 | 22 | // OverlayPushArea pushes area overlay for the next frame. 23 | func (e *Editor) OverlayPushArea(area util.Bounds, colFill, colBorder util.Color) { 24 | e.pMap.PushAreaHover(util.Bounds{ 25 | X1: (area.X1 - 1) * float32(dmmap.WorldIconSize), 26 | Y1: (area.Y1 - 1) * float32(dmmap.WorldIconSize), 27 | X2: (area.X2-1)*float32(dmmap.WorldIconSize) + float32(dmmap.WorldIconSize), 28 | Y2: (area.Y2-1)*float32(dmmap.WorldIconSize) + float32(dmmap.WorldIconSize), 29 | }, colFill, colBorder) 30 | } 31 | 32 | // OverlaySetTileFlick sets for the provided tile a flick overlay. 33 | // Unlike the PushOverlayTile or PushOverlayArea methods, flick overlay is set only once. 34 | // It will exist until it disappears. 35 | func (e *Editor) OverlaySetTileFlick(coord util.Point) { 36 | e.flickAreas = append(e.flickAreas, overlay.FlickArea{ 37 | Time: imgui.Time(), 38 | Area: util.Bounds{ 39 | X1: float32((coord.X - 1) * dmmap.WorldIconSize), 40 | Y1: float32((coord.Y - 1) * dmmap.WorldIconSize), 41 | X2: float32((coord.X-1)*dmmap.WorldIconSize + dmmap.WorldIconSize), 42 | Y2: float32((coord.Y-1)*dmmap.WorldIconSize + dmmap.WorldIconSize), 43 | }, 44 | }) 45 | } 46 | 47 | // OverlaySetInstanceFlick sets for the provided instance a flick overlay. 48 | // Unlike the PushOverlayTile or PushOverlayArea methods, flick overlay is set only once. 49 | // It will exist until it disappears. 50 | func (e *Editor) OverlaySetInstanceFlick(i *dmminstance.Instance) { 51 | e.flickInstance = append(e.flickInstance, overlay.FlickInstance{ 52 | Time: imgui.Time(), 53 | Instance: i, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/editor/zones.go: -------------------------------------------------------------------------------- 1 | package editor 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dm" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | type AreaZone struct { 9 | Name string 10 | Borders []AreaBorder 11 | } 12 | 13 | type AreaBorder struct { 14 | Coord util.Point 15 | Dirs int 16 | } 17 | 18 | //nolint:gofmt 19 | var zoneDirs = map[util.Point]int{ 20 | util.Point{X: 1}: dm.DirEast, 21 | util.Point{X: -1}: dm.DirWest, 22 | util.Point{Y: 1}: dm.DirNorth, 23 | util.Point{Y: -1}: dm.DirSouth, 24 | } 25 | 26 | func (e *Editor) updateAreasZones() { 27 | type coords map[util.Point]bool 28 | 29 | areas := make(map[string]coords) 30 | 31 | for _, tile := range e.dmm.Tiles { 32 | for _, instance := range tile.Instances() { 33 | if path := instance.Prefab().Path(); dm.IsPath(path, "/area") { 34 | if _, ok := areas[path]; !ok { 35 | areas[path] = make(coords) 36 | } 37 | areas[path][tile.Coord] = true 38 | } 39 | } 40 | } 41 | 42 | var areaZones []AreaZone 43 | 44 | for areaName, areaCoords := range areas { 45 | areaZone := AreaZone{Name: areaName} 46 | 47 | for coord := range areaCoords { 48 | var areaBorder AreaBorder 49 | 50 | for shift, dir := range zoneDirs { 51 | if _, ok := areaCoords[coord.Plus(shift)]; !ok { 52 | areaBorder.Coord = coord 53 | areaBorder.Dirs |= dir 54 | } 55 | } 56 | 57 | if !areaBorder.Coord.Equals(0, 0, 0) { 58 | areaZone.Borders = append(areaZone.Borders, areaBorder) 59 | } 60 | } 61 | 62 | areaZones = append(areaZones, areaZone) 63 | } 64 | 65 | e.areasZones = areaZones 66 | } 67 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/overlay/color.go: -------------------------------------------------------------------------------- 1 | package overlay 2 | 3 | import ( 4 | "sdmm/internal/imguiext/style" 5 | "sdmm/internal/util" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | ) 9 | 10 | var ( 11 | ColorEmpty = util.Color{} 12 | 13 | ColorToolAddTileFill = util.MakeColor(1, 1, 1, 0.25) 14 | ColorToolAddAltTileFill = util.MakeColor(1, 1, 1, 0.25) 15 | ColorToolAddTileBorder = util.MakeColor(1, 1, 1, 1) 16 | ColorToolAddAltTileBorder = util.MakeColorFromVec4(style.ColorGold) 17 | 18 | ColorToolFillTileFill = util.MakeColor(1, 1, 1, 0.25) 19 | ColorToolFillAltTileFill = util.MakeColorFromVec4(style.ColorGold.Minus(imgui.Vec4{W: 0.75})) 20 | ColorToolFillTileBorder = ColorEmpty 21 | ColorToolFillAltTileBorder = ColorEmpty 22 | 23 | ColorToolSelectTileFill = util.MakeColor(1, 1, 1, 0.25) 24 | ColorToolSelectTileBorder = util.MakeColor(0, 1, 0, 1) 25 | 26 | ColorToolPickInstance = util.MakeColor(0, 1, 0, 1) 27 | 28 | ColorToolDeleteInstance = util.MakeColor(1, 0, 0, 1) 29 | ColorToolDeleteAltTileFill = util.MakeColor(1, 0, 0, 0.25) 30 | ColorToolDeleteAltTileBorder = util.MakeColorFromVec4(style.ColorGold) 31 | 32 | ColorToolReplaceInstance = util.MakeColor(0, 1, 0, 1) 33 | 34 | ColorFlickTileFill = util.MakeColor(1, 1, 1, 1) 35 | ColorFlickInstance = util.MakeColor(0, 1, 0, 1) 36 | 37 | ColorAreaBorder = util.MakeColor(1, 1, 1, 1) 38 | ) 39 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/overlay/overlay.go: -------------------------------------------------------------------------------- 1 | package overlay 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmmap/dmminstance" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | type FlickArea struct { 9 | Time float64 10 | Area util.Bounds 11 | } 12 | 13 | type FlickInstance struct { 14 | Time float64 15 | Instance *dmminstance.Instance 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/panel.go: -------------------------------------------------------------------------------- 1 | package pmap 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | const ( 6 | panelPadding float32 = 5 7 | panelAlpha float32 = .75 8 | panelRounding float32 = 1 9 | 10 | panelFlags = imgui.WindowFlagsNoResize | imgui.WindowFlagsAlwaysAutoResize | 11 | imgui.WindowFlagsNoTitleBar | imgui.WindowFlagsNoMove | 12 | imgui.WindowFlagsNoSavedSettings | imgui.WindowFlagsNoDocking | imgui.WindowFlagsNoFocusOnAppearing 13 | ) 14 | 15 | type panelPos int 16 | 17 | const ( 18 | pPosTop panelPos = iota 19 | pPosRightTop 20 | pPosRightBottom 21 | pPosBottom 22 | ) 23 | 24 | func (p *PaneMap) showPanel(id string, panelPos panelPos, content func()) { 25 | p.showPanelV(id, panelPos, true, content) 26 | } 27 | 28 | func (p *PaneMap) showPanelV(id string, panelPos panelPos, visible bool, content func()) { 29 | if !visible { 30 | return 31 | } 32 | 33 | var pos, size imgui.Vec2 34 | 35 | switch panelPos { 36 | case pPosTop: 37 | pos = p.pos.Plus(imgui.Vec2{X: panelPadding, Y: panelPadding}) 38 | size = imgui.Vec2{X: p.size.X - panelPadding*2} 39 | case pPosRightTop: 40 | x := imgui.ContentRegionAvail().X - p.panelRightTopSize.X - panelPadding 41 | y := p.panelBottomSize.Y + panelPadding*2 42 | pos = p.pos.Plus(imgui.Vec2{X: x, Y: y}) 43 | case pPosRightBottom: 44 | x := imgui.ContentRegionAvail().X - p.panelRightBottomSize.X - panelPadding 45 | y := imgui.ContentRegionAvail().Y - p.panelRightBottomSize.Y - p.panelBottomSize.Y - panelPadding*2 46 | pos = p.pos.Plus(imgui.Vec2{X: x, Y: y}) 47 | case pPosBottom: 48 | y := imgui.ContentRegionAvail().Y - p.panelBottomSize.Y - panelPadding 49 | pos = p.pos.Plus(imgui.Vec2{X: panelPadding, Y: y}) 50 | size = imgui.Vec2{X: p.size.X - panelPadding*2} 51 | } 52 | 53 | imgui.SetNextWindowPos(pos) 54 | imgui.SetNextWindowSize(size) 55 | imgui.SetNextWindowBgAlpha(panelAlpha) 56 | imgui.PushStyleVarFloat(imgui.StyleVarWindowRounding, panelRounding) 57 | 58 | if imgui.BeginV(id, nil, panelFlags) { 59 | imgui.PopStyleVar() 60 | 61 | p.updateShortcutsState() 62 | p.focused = p.focused || imgui.IsWindowFocusedV(imgui.FocusedFlagsRootAndChildWindows) 63 | 64 | content() 65 | 66 | switch panelPos { 67 | case pPosTop: 68 | p.panelTopSize = imgui.WindowSize() 69 | case pPosRightTop: 70 | p.panelRightTopSize = imgui.WindowSize() 71 | case pPosRightBottom: 72 | p.panelRightBottomSize = imgui.WindowSize() 73 | case pPosBottom: 74 | p.panelBottomSize = imgui.WindowSize() 75 | } 76 | } else { 77 | imgui.PopStyleVar() 78 | } 79 | imgui.End() 80 | } 81 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/panel_status.go: -------------------------------------------------------------------------------- 1 | package pmap 2 | 3 | import ( 4 | "fmt" 5 | 6 | "sdmm/internal/app/ui/cpwsarea/wsmap/tools" 7 | "sdmm/internal/app/ui/shortcut" 8 | "sdmm/internal/imguiext/icon" 9 | w "sdmm/internal/imguiext/widget" 10 | "sdmm/internal/platform" 11 | ) 12 | 13 | func (p *PaneMap) showStatusPanel() { 14 | w.Layout{ 15 | p.panelStatusLayoutStatus(), 16 | w.SameLine(), 17 | w.Custom(func() { 18 | if p.dmm.MaxZ != 1 { 19 | w.Layout{ 20 | p.panelStatusLayoutLevels(), 21 | }.BuildV(w.AlignRight) 22 | } 23 | }), 24 | }.Build() 25 | } 26 | 27 | func (p *PaneMap) panelStatusLayoutStatus() (layout w.Layout) { 28 | if p.canvasState.HoverOutOfBounds() { 29 | layout = append(layout, w.TextFrame("out of bounds")) 30 | } else { 31 | t := p.canvasState.HoveredTile() 32 | layout = append(layout, w.TextFrame(fmt.Sprintf("X:%03d Y:%03d", t.X, t.Y))) 33 | } 34 | 35 | layout = append(layout, w.Tooltip(w.Text("Tile coordinates of the mouse"))) 36 | 37 | if isQuickToolToggled() && !tools.Selected().AltBehaviour() { 38 | if hoveredInstance := p.canvasState.HoveredInstance(); hoveredInstance != nil { 39 | layout = append(layout, w.TextFrame(hoveredInstance.Prefab().Path())) 40 | } 41 | } else if tool, ok := tools.Selected().(*tools.ToolGrab); ok && tool.HasSelectedArea() { 42 | bounds := tool.Bounds() 43 | layout = append(layout, 44 | w.TextFrame(fmt.Sprintf("W:%d H:%d", int(bounds.X2-bounds.X1)+1, int(bounds.Y2-bounds.Y1)+1)), 45 | w.Tooltip(w.Text("Grab area size")), 46 | w.TextFrame(bounds.String()), 47 | w.Tooltip(w.Text("Grab area bounds")), 48 | ) 49 | } 50 | 51 | return w.Layout{ 52 | w.Line(layout...), 53 | } 54 | } 55 | 56 | func isQuickToolToggled() bool { 57 | return tools.IsSelected(tools.TNPick) || tools.IsSelected(tools.TNDelete) || tools.IsSelected(tools.TNReplace) 58 | } 59 | 60 | func (p *PaneMap) panelStatusLayoutLevels() (layout w.Layout) { 61 | return w.Layout{ 62 | w.TextFrame(fmt.Sprintf("Z:%d", p.activeLevel)), 63 | w.Tooltip(w.Text("Current Z-level")).OnHover(true), 64 | w.SameLine(), 65 | w.Disabled(!p.hasPreviousLevel(), w.Layout{ 66 | w.Button(icon.ArrowDownward, p.doPreviousLevel). 67 | Tooltip(fmt.Sprintf("Previous z-level (%s)", shortcut.Combine(platform.KeyModName(), "Down"))). 68 | Round(true), 69 | }), 70 | w.SameLine(), 71 | w.Disabled(!p.hasNextLevel(), w.Layout{ 72 | w.Button(icon.ArrowUpward, p.doNextLevel). 73 | Tooltip(fmt.Sprintf("Next z-level (%s)", shortcut.Combine(platform.KeyModName(), "Up"))). 74 | Round(true), 75 | }), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/psettings/config.go: -------------------------------------------------------------------------------- 1 | package psettings 2 | 3 | import ( 4 | "os/user" 5 | 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | const ( 10 | configName = "psettings" 11 | configVersion = 1 12 | ) 13 | 14 | type psettingsConfig struct { 15 | Version uint 16 | 17 | ScreenshotDir string 18 | 19 | InSelectionMode bool 20 | ToClipboardMode bool 21 | } 22 | 23 | func (psettingsConfig) Name() string { 24 | return configName 25 | } 26 | 27 | func (psettingsConfig) TryMigrate(_ map[string]any) (result map[string]any, migrated bool) { 28 | // do nothing. yet... 29 | return nil, migrated 30 | } 31 | 32 | func loadConfig(app App) *psettingsConfig { 33 | u, err := user.Current() 34 | if err != nil { 35 | log.Fatal().Msgf("unable to find user: %v", err) 36 | } 37 | 38 | cfg := &psettingsConfig{ 39 | Version: configVersion, 40 | 41 | ScreenshotDir: u.HomeDir, 42 | ToClipboardMode: true, 43 | } 44 | app.ConfigRegister(cfg) 45 | return cfg 46 | } 47 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/psettings/map_size.go: -------------------------------------------------------------------------------- 1 | package psettings 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "sdmm/internal/imguiext" 8 | "sdmm/internal/imguiext/style" 9 | w "sdmm/internal/imguiext/widget" 10 | 11 | "github.com/SpaiR/imgui-go" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | const ( 16 | possibleMaxX = math.MaxInt 17 | possibleMaxY = math.MaxInt 18 | possibleMaxZ = math.MaxInt 19 | ) 20 | 21 | type sessionMapSize struct { 22 | maxX, maxY, maxZ int32 23 | } 24 | 25 | func (s sessionMapSize) String() string { 26 | return fmt.Sprintf("maxX: %d, maxY: %d, maxZ: %d", s.maxX, s.maxY, s.maxZ) 27 | } 28 | 29 | func (p *Panel) DropSessionMapSize() { 30 | p.sessionMapSize = nil 31 | } 32 | 33 | func (p *Panel) showMapSize() { 34 | if imgui.CollapsingHeader("Map Size") { 35 | if p.sessionMapSize == nil { 36 | p.sessionMapSize = &sessionMapSize{ 37 | maxX: int32(p.editor.Dmm().MaxX), 38 | maxY: int32(p.editor.Dmm().MaxY), 39 | maxZ: int32(p.editor.Dmm().MaxZ), 40 | } 41 | } 42 | 43 | imgui.AlignTextToFramePadding() 44 | imgui.Text("X") 45 | imgui.SameLine() 46 | imgui.SetNextItemWidth(-1) 47 | imguiext.InputIntClamp("##max_x", &p.sessionMapSize.maxX, 1, possibleMaxX, 1, 10) 48 | 49 | imgui.AlignTextToFramePadding() 50 | imgui.Text("Y") 51 | imgui.SameLine() 52 | imgui.SetNextItemWidth(-1) 53 | imguiext.InputIntClamp("##max_y", &p.sessionMapSize.maxY, 1, possibleMaxY, 1, 10) 54 | 55 | imgui.AlignTextToFramePadding() 56 | imgui.Text("Z") 57 | imgui.SameLine() 58 | imgui.SetNextItemWidth(-1) 59 | imguiext.InputIntClamp("##max_z", &p.sessionMapSize.maxZ, 1, possibleMaxZ, 1, 10) 60 | 61 | imgui.Separator() 62 | 63 | w.Button("Set", p.doSetMapSize). 64 | Size(imgui.Vec2{X: -1}). 65 | Style(style.ButtonGreen{}). 66 | Build() 67 | } else { 68 | p.sessionMapSize = nil 69 | } 70 | } 71 | 72 | func (p *Panel) doSetMapSize() { 73 | log.Printf("do set map size [%s]: %v", p.editor.Dmm().Name, p.sessionMapSize) 74 | oldMaxX, oldMaxY, oldMaxZ := p.editor.Dmm().MaxX, p.editor.Dmm().MaxY, p.editor.Dmm().MaxZ 75 | p.editor.Dmm().SetMapSize(int(p.sessionMapSize.maxX), int(p.sessionMapSize.maxY), int(p.sessionMapSize.maxZ)) 76 | p.editor.CommitMapSizeChange(oldMaxX, oldMaxY, oldMaxZ) 77 | p.sessionMapSize = nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/psettings/psettings.go: -------------------------------------------------------------------------------- 1 | package psettings 2 | 3 | import ( 4 | "sdmm/internal/app/config" 5 | "sdmm/internal/app/window" 6 | "sdmm/internal/dmapi/dm" 7 | "sdmm/internal/dmapi/dmmap" 8 | 9 | "github.com/SpaiR/imgui-go" 10 | ) 11 | 12 | type App interface { 13 | PathsFilter() *dm.PathsFilter 14 | 15 | ConfigRegister(config.Config) 16 | } 17 | 18 | type editor interface { 19 | ActiveLevel() int 20 | 21 | Dmm() *dmmap.Dmm 22 | CommitMapSizeChange(oldMaxX, oldMaxY, oldMaxZ int) 23 | } 24 | 25 | type Panel struct { 26 | app App 27 | 28 | editor editor 29 | 30 | sessionMapSize *sessionMapSize 31 | sessionScreenshot *sessionScreenshot 32 | } 33 | 34 | var cfg *psettingsConfig 35 | 36 | func New(app App, editor editor) *Panel { 37 | if cfg == nil { 38 | cfg = loadConfig(app) 39 | } 40 | return &Panel{app: app, editor: editor, sessionScreenshot: &sessionScreenshot{}} 41 | } 42 | 43 | func (p *Panel) Process() { 44 | imgui.Dummy(imgui.Vec2{X: p.headerSize()}) 45 | p.showMapSize() 46 | p.showScreenshot() 47 | } 48 | 49 | func (p *Panel) headerSize() float32 { 50 | return window.PointSize() * 150 51 | } 52 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/tilemenu/shortcut.go: -------------------------------------------------------------------------------- 1 | package tilemenu 2 | 3 | import ( 4 | "sdmm/internal/app/ui/shortcut" 5 | 6 | "github.com/go-gl/glfw/v3.3/glfw" 7 | ) 8 | 9 | func (t *TileMenu) addShortcuts() { 10 | t.shortcuts.Add(shortcut.Shortcut{ 11 | Name: "tileMenu#close", 12 | FirstKey: glfw.KeyEscape, 13 | Action: t.close, 14 | IsEnabled: func() bool { return t.opened }, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/tilemenu/tilemenu.go: -------------------------------------------------------------------------------- 1 | package tilemenu 2 | 3 | import ( 4 | "sdmm/internal/app/command" 5 | "sdmm/internal/app/prefs" 6 | "sdmm/internal/app/ui/cpwsarea/wsmap/pmap/pquickedit" 7 | "sdmm/internal/app/ui/shortcut" 8 | "sdmm/internal/dmapi/dmenv" 9 | "sdmm/internal/dmapi/dmmap" 10 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 11 | "sdmm/internal/dmapi/dmmap/dmminstance" 12 | "sdmm/internal/dmapi/dmmclip" 13 | "sdmm/internal/util" 14 | 15 | "github.com/SpaiR/imgui-go" 16 | ) 17 | 18 | type App interface { 19 | DoUndo() 20 | DoRedo() 21 | 22 | DoCopy() 23 | DoPaste() 24 | DoCut() 25 | DoDelete() 26 | 27 | DoSearchPrefab(prefabId uint64) 28 | DoSearchPrefabByPath(path string) 29 | 30 | ShowLayout(name string, focus bool) 31 | 32 | CommandStorage() *command.Storage 33 | Clipboard() *dmmclip.Clipboard 34 | 35 | HasSelectedPrefab() bool 36 | SelectedPrefab() (*dmmprefab.Prefab, bool) 37 | 38 | SelectedInstance() (*dmminstance.Instance, bool) 39 | 40 | Prefs() prefs.Prefs 41 | LoadedEnvironment() *dmenv.Dme 42 | } 43 | 44 | type editor interface { 45 | Dmm() *dmmap.Dmm 46 | 47 | CommitChanges(string) 48 | 49 | InstanceSelect(i *dmminstance.Instance) 50 | InstanceMoveToTop(i *dmminstance.Instance) 51 | InstanceMoveToBottom(i *dmminstance.Instance) 52 | InstanceDelete(i *dmminstance.Instance) 53 | InstanceReplace(i *dmminstance.Instance, prefab *dmmprefab.Prefab) 54 | InstanceReset(i *dmminstance.Instance) 55 | 56 | UpdateCanvasByCoords([]util.Point) 57 | } 58 | 59 | type TileMenu struct { 60 | shortcuts shortcut.Shortcuts 61 | 62 | app App 63 | editor editor 64 | 65 | opened bool 66 | 67 | tile *dmmap.Tile 68 | 69 | pQuickEdit *pquickedit.Panel 70 | } 71 | 72 | func New(app App, editor editor) *TileMenu { 73 | t := &TileMenu{app: app, editor: editor, pQuickEdit: pquickedit.New(app, editor)} 74 | t.addShortcuts() 75 | return t 76 | } 77 | 78 | func (t *TileMenu) Dispose() { 79 | t.close() 80 | t.shortcuts.Dispose() 81 | } 82 | 83 | func (t *TileMenu) Open(coord util.Point) { 84 | if t.editor.Dmm().HasTile(coord) { 85 | t.tile = t.editor.Dmm().GetTile(coord) 86 | t.opened = true 87 | imgui.OpenPopup("tileMenu") 88 | } 89 | } 90 | 91 | func (t *TileMenu) close() { 92 | t.opened = false 93 | t.tile = nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/tools.go: -------------------------------------------------------------------------------- 1 | package pmap 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/wsmap/tools" 5 | "sdmm/internal/app/window" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | "github.com/go-gl/glfw/v3.3/glfw" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | func init() { 13 | window.RunRepeat(func() { 14 | processTempToolsMode() 15 | }) 16 | } 17 | 18 | var ( 19 | tmpToolIsInTemporalMode bool 20 | tmpToolLastSelectedName string 21 | tmpToolPrevSelectedName string 22 | ) 23 | 24 | func processTempToolsMode() { 25 | if !tmpToolIsInTemporalMode { 26 | tmpToolLastSelectedName = tools.Selected().Name() 27 | } 28 | 29 | var inMode bool 30 | inMode = inMode || processTempToolMode(int(glfw.KeyS), -1, tools.TNPick) 31 | inMode = inMode || processTempToolMode(int(glfw.KeyD), -1, tools.TNDelete) 32 | inMode = inMode || processTempToolMode(int(glfw.KeyR), -1, tools.TNReplace) 33 | 34 | if tmpToolIsInTemporalMode && !inMode { 35 | log.Print("select before-tmp tool:", tmpToolLastSelectedName) 36 | tools.SetSelected(tmpToolLastSelectedName) 37 | tmpToolLastSelectedName = "" 38 | tmpToolIsInTemporalMode = false 39 | } 40 | } 41 | 42 | func processTempToolMode(key, altKey int, modeName string) bool { 43 | // Ignore presses when Dear ImGui inputs are in charge or actual shortcuts are invisible. 44 | { 45 | var p *PaneMap 46 | if activePane != nil { 47 | p = activePane 48 | } else if lastActivePane != nil { 49 | p = lastActivePane 50 | } 51 | if p != nil && !(p.canvasControl.Active() || p.shortcuts.Visible()) { 52 | return false 53 | } 54 | } 55 | 56 | isKeyPressed := imgui.IsKeyPressedV(key, false) || imgui.IsKeyPressedV(altKey, false) 57 | isKeyReleased := imgui.IsKeyReleased(key) || imgui.IsKeyReleased(altKey) 58 | isKeyDown := imgui.IsKeyDown(key) || imgui.IsKeyDown(altKey) 59 | isSelected := tools.IsSelected(modeName) 60 | 61 | if isKeyPressed && !isSelected { 62 | log.Print("selecting tmp tool:", modeName) 63 | tmpToolPrevSelectedName = tools.Selected().Name() 64 | tmpToolIsInTemporalMode = true 65 | tools.SetSelected(modeName) 66 | } else if isKeyReleased && len(tmpToolPrevSelectedName) != 0 { 67 | if isSelected { 68 | log.Print("selecting prev-tmp tool:", tmpToolPrevSelectedName) 69 | tools.SetSelected(tmpToolPrevSelectedName) 70 | } 71 | tmpToolPrevSelectedName = modeName 72 | } 73 | 74 | return isKeyDown 75 | } 76 | 77 | func selectAddTool() { 78 | tools.SetSelected(tools.TNAdd) 79 | } 80 | 81 | func selectFillTool() { 82 | tools.SetSelected(tools.TNFill) 83 | } 84 | 85 | func selectSelectTool() { 86 | tools.SetSelected(tools.TNGrab) 87 | } 88 | 89 | func selectMoveTool() { 90 | tools.SetSelected(tools.TNMove) 91 | } 92 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/pmap/unit_processor.go: -------------------------------------------------------------------------------- 1 | package pmap 2 | 3 | import ( 4 | "sdmm/internal/app/render/bucket/level/chunk/unit" 5 | ) 6 | 7 | func (p *PaneMap) ProcessUnit(u unit.Unit) bool { 8 | if p.app.PathsFilter().IsHiddenPath(u.Instance().Prefab().Path()) { 9 | return false 10 | } 11 | p.locateHoveredInstance(u) 12 | return true 13 | } 14 | 15 | func (p *PaneMap) locateHoveredInstance(u unit.Unit) { 16 | mouseX, mouseY := p.canvasState.RelMouseX(), p.canvasState.RelMouseY() 17 | 18 | if u.ViewBounds().Contains(float32(mouseX), float32(mouseY)) { 19 | xOffset := int(float32(mouseX)-u.ViewBounds().X1) + u.Sprite().X1 20 | yOffset := u.Sprite().IconHeight() - 1 - int(float32(mouseY)-u.ViewBounds().Y1) + u.Sprite().Y1 21 | if _, _, _, a := u.Sprite().Image().At(xOffset, yOffset).RGBA(); a != 0 { 22 | p.tmpLastHoveredInstance = u.Instance() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/save.go: -------------------------------------------------------------------------------- 1 | package wsmap 2 | 3 | import ( 4 | "sdmm/internal/app/prefs" 5 | "sdmm/internal/dmapi/dmmsave" 6 | 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | func (ws *WsMap) Save() bool { 11 | log.Print("saving map workspace:", ws.CommandStackId()) 12 | 13 | editorPrefs := ws.app.Prefs().Editor 14 | 15 | var saveFormat dmmsave.Format 16 | switch editorPrefs.SaveFormat { 17 | case prefs.SaveFormatInitial: 18 | saveFormat = dmmsave.FormatInitial 19 | case prefs.SaveFormatTGM: 20 | saveFormat = dmmsave.FormatTGM 21 | case prefs.SaveFormatDMM: 22 | saveFormat = dmmsave.FormatDM 23 | } 24 | 25 | dmmsave.Save(ws.app.LoadedEnvironment(), ws.paneMap.Dmm(), dmmsave.Config{ 26 | Format: saveFormat, 27 | SanitizeVariables: editorPrefs.SanitizeVariables, 28 | }) 29 | 30 | ws.app.CommandStorage().ForceBalance(ws.CommandStackId()) 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/tools/add.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/wsmap/pmap/overlay" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | // ToolAdd can be used to add prefabs to the map. 9 | // During mouse moving when the tool is active a selected prefab will be added on every tile under the mouse. 10 | // You can't add the same prefab twice on the same tile during the one OnStart -> OnStop cycle. 11 | // 12 | // Default: obj placed on top, area and turfs are replaced. 13 | // Alternative: obj replaced, area and turfs are placed on top. 14 | type ToolAdd struct { 15 | tool 16 | 17 | editedTiles map[util.Point]bool 18 | } 19 | 20 | func (ToolAdd) Name() string { 21 | return TNAdd 22 | } 23 | 24 | func newAdd() *ToolAdd { 25 | return &ToolAdd{ 26 | editedTiles: make(map[util.Point]bool), 27 | } 28 | } 29 | 30 | func (t *ToolAdd) process() { 31 | for coord := range t.editedTiles { 32 | if t.AltBehaviour() { 33 | ed.OverlayPushTile(coord, overlay.ColorToolAddAltTileFill, overlay.ColorToolAddAltTileBorder) 34 | } else { 35 | ed.OverlayPushTile(coord, overlay.ColorToolAddTileFill, overlay.ColorToolAddTileBorder) 36 | } 37 | } 38 | } 39 | 40 | func (t *ToolAdd) onStart(coord util.Point) { 41 | t.onMove(coord) 42 | } 43 | 44 | func (t *ToolAdd) onMove(coord util.Point) { 45 | if prefab, ok := ed.SelectedPrefab(); ok && !t.editedTiles[coord] { 46 | t.editedTiles[coord] = true // Don't add to the same tile twice 47 | 48 | tile := ed.Dmm().GetTile(coord) 49 | t.basicPrefabAdd(tile, prefab) 50 | 51 | ed.UpdateCanvasByCoords([]util.Point{coord}) 52 | } 53 | } 54 | 55 | func (t *ToolAdd) onStop(util.Point) { 56 | if len(t.editedTiles) != 0 { 57 | t.editedTiles = make(map[util.Point]bool, len(t.editedTiles)) 58 | go ed.CommitChanges("Add Atoms") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/tools/delete.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "sdmm/internal/app/ui/cpwsarea/wsmap/pmap/overlay" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | // ToolDelete can be used to delete a hovered object instance. 9 | // It has an alt behaviour which is able to delete all instances on the tile. 10 | type ToolDelete struct { 11 | tool 12 | 13 | deletedTiles map[util.Point]bool 14 | } 15 | 16 | func (t ToolDelete) IgnoreBounds() bool { 17 | return !t.AltBehaviour() 18 | } 19 | 20 | func (ToolDelete) Name() string { 21 | return TNDelete 22 | } 23 | 24 | func newDelete() *ToolDelete { 25 | return &ToolDelete{ 26 | deletedTiles: make(map[util.Point]bool), 27 | } 28 | } 29 | 30 | func (t *ToolDelete) process() { 31 | for coord := range t.deletedTiles { 32 | if t.AltBehaviour() { 33 | ed.OverlayPushTile(coord, overlay.ColorToolDeleteAltTileFill, overlay.ColorToolDeleteAltTileBorder) 34 | } 35 | } 36 | } 37 | 38 | func (t *ToolDelete) onStart(coord util.Point) { 39 | if t.AltBehaviour() { 40 | t.onMove(coord) 41 | } else if hoveredInstance := ed.HoveredInstance(); hoveredInstance != nil { 42 | ed.InstanceDelete(hoveredInstance) 43 | go ed.CommitChanges("Delete Instance") 44 | } 45 | } 46 | 47 | func (t *ToolDelete) onMove(coord util.Point) { 48 | if t.AltBehaviour() && !t.deletedTiles[coord] { 49 | t.deletedTiles[coord] = true // Don't delete to the same tile twice 50 | ed.TileDeleteSelected() 51 | ed.UpdateCanvasByCoords([]util.Point{coord}) 52 | } 53 | } 54 | 55 | func (t *ToolDelete) onStop(util.Point) { 56 | if len(t.deletedTiles) != 0 { 57 | t.deletedTiles = make(map[util.Point]bool, len(t.deletedTiles)) 58 | go ed.CommitChanges("Delete Tiles") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/tools/pick.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "sdmm/internal/util" 5 | ) 6 | 7 | // ToolPick can be used to select a hovered object instance. 8 | type ToolPick struct { 9 | tool 10 | } 11 | 12 | func (ToolPick) Name() string { 13 | return TNPick 14 | } 15 | 16 | func newPick() *ToolPick { 17 | return &ToolPick{} 18 | } 19 | 20 | func (ToolPick) IgnoreBounds() bool { 21 | return true 22 | } 23 | 24 | func (ToolPick) AltBehaviour() bool { 25 | return false 26 | } 27 | 28 | func (t ToolPick) onStart(util.Point) { 29 | if hoveredInstance := ed.HoveredInstance(); hoveredInstance != nil { 30 | ed.InstanceSelect(hoveredInstance) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/tools/replace.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "sdmm/internal/util" 5 | ) 6 | 7 | // ToolReplace can be used to replace the hovered instance with the selected prefab. 8 | type ToolReplace struct { 9 | tool 10 | } 11 | 12 | func (ToolReplace) Name() string { 13 | return TNReplace 14 | } 15 | 16 | func newReplace() *ToolReplace { 17 | return &ToolReplace{} 18 | } 19 | 20 | func (ToolReplace) IgnoreBounds() bool { 21 | return true 22 | } 23 | 24 | func (ToolReplace) AltBehaviour() bool { 25 | return false 26 | } 27 | 28 | func (t ToolReplace) onStart(util.Point) { 29 | if hoveredInstance := ed.HoveredInstance(); hoveredInstance != nil { 30 | if selectedPrefab, ok := ed.SelectedPrefab(); ok { 31 | hoveredInstance.SetPrefab(selectedPrefab) 32 | ed.CommitChanges("Replace Instance") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsmap/tools/tool.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dm" 5 | "sdmm/internal/dmapi/dmmap" 6 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 7 | "sdmm/internal/util" 8 | ) 9 | 10 | // Tool is a basic interface for tools in the panel. 11 | type Tool interface { 12 | Name() string 13 | 14 | IgnoreBounds() bool 15 | Stale() bool 16 | AltBehaviour() bool 17 | setAltBehaviour(bool) 18 | 19 | // OnDeselect gees when the current tool is deselected. 20 | OnDeselect() 21 | 22 | // Goes every app cycle to handle stuff like pushing overlays etc. 23 | process() 24 | // Goes when user clicks on the map. 25 | onStart(coord util.Point) 26 | // Goes when user clicked and, while holding the mouse button, move the mouse. 27 | onMove(coord util.Point) 28 | // Goes when user releases the mouse button. 29 | onStop(coord util.Point) 30 | } 31 | 32 | // Tool is a basic interface for tools in the panel. 33 | type tool struct { 34 | altBehaviour bool 35 | } 36 | 37 | func (tool) IgnoreBounds() bool { 38 | return false 39 | } 40 | 41 | func (tool) Stale() bool { 42 | return true 43 | } 44 | 45 | func (t *tool) AltBehaviour() bool { 46 | return t.altBehaviour 47 | } 48 | 49 | func (t *tool) setAltBehaviour(altBehaviour bool) { 50 | t.altBehaviour = altBehaviour 51 | } 52 | 53 | func (tool) process() { 54 | } 55 | 56 | //nolint:unused 57 | func (tool) onStart(util.Point) { 58 | } 59 | 60 | func (tool) onMove(util.Point) { 61 | } 62 | 63 | func (tool) onStop(util.Point) { 64 | } 65 | 66 | func (tool) OnDeselect() { 67 | } 68 | 69 | // A basic behaviour add. 70 | // Adds object above and tile with a replacement. 71 | // Mirrors that behaviour in the alt mode. 72 | func (t *tool) basicPrefabAdd(tile *dmmap.Tile, prefab *dmmprefab.Prefab) { 73 | if !t.altBehaviour { 74 | if dm.IsPath(prefab.Path(), "/area") { 75 | tile.InstancesRemoveByPath("/area") 76 | } else if dm.IsPath(prefab.Path(), "/turf") { 77 | tile.InstancesRemoveByPath("/turf") 78 | } 79 | } else if dm.IsPath(prefab.Path(), "/obj") { 80 | tile.InstancesRemoveByPath("/obj") 81 | } 82 | 83 | tile.InstancesAdd(prefab) 84 | tile.InstancesRegenerate() 85 | } 86 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsprefs/pref.go: -------------------------------------------------------------------------------- 1 | package wsprefs 2 | 3 | import "math" 4 | 5 | type basePref struct { 6 | Name string 7 | Desc string 8 | Label string 9 | Help string 10 | } 11 | 12 | type IntPref struct { 13 | basePref 14 | 15 | FGet func() int 16 | FSet func(int) 17 | 18 | Min, Max int 19 | Step, StepFast int 20 | } 21 | 22 | func MakeIntPref() IntPref { 23 | return IntPref{ 24 | Min: math.MinInt, 25 | Max: math.MaxInt, 26 | Step: 1, 27 | StepFast: 10, 28 | } 29 | } 30 | 31 | type BoolPref struct { 32 | basePref 33 | 34 | FGet func() bool 35 | FSet func(bool) 36 | } 37 | 38 | func MakeBoolPref() BoolPref { 39 | return BoolPref{} 40 | } 41 | 42 | type OptionPref struct { 43 | basePref 44 | 45 | FGet func() string 46 | FSet func(string) 47 | 48 | Options []string 49 | } 50 | 51 | func MakeOptionPref() OptionPref { 52 | return OptionPref{} 53 | } 54 | -------------------------------------------------------------------------------- /internal/app/ui/cpwsarea/wsprefs/prefs.go: -------------------------------------------------------------------------------- 1 | package wsprefs 2 | 3 | type PrefGroup string 4 | 5 | const ( 6 | GPEditor PrefGroup = "Editor" 7 | GPControls PrefGroup = "Controls" 8 | GPInterface PrefGroup = "Interface" 9 | GPApplication PrefGroup = "Application" 10 | ) 11 | 12 | var prefsGroupOrder = []PrefGroup{ 13 | GPEditor, 14 | GPControls, 15 | GPInterface, 16 | GPApplication, 17 | } 18 | 19 | type Prefs map[PrefGroup][]any 20 | 21 | func MakePrefs() Prefs { 22 | return make(Prefs) 23 | } 24 | 25 | func (p Prefs) Add(group PrefGroup, pref any) { 26 | p[group] = append(p[group], pref) 27 | } 28 | -------------------------------------------------------------------------------- /internal/app/ui/dialog/confirmation.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "sdmm/internal/imguiext/style" 5 | w "sdmm/internal/imguiext/widget" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | ) 9 | 10 | type TypeConfirmation struct { 11 | Title string 12 | Question string 13 | ActionYes func() 14 | ActionNo func() 15 | ActionCancel func() 16 | } 17 | 18 | func (t TypeConfirmation) Name() string { 19 | return t.Title 20 | } 21 | 22 | func (TypeConfirmation) HasCloseButton() bool { 23 | return false 24 | } 25 | 26 | func (t TypeConfirmation) Process() { 27 | w.Layout{ 28 | w.Text(t.Question), 29 | w.Separator(), 30 | w.Button("Yes", t.doYes). 31 | Style(style.ButtonGreen{}), 32 | w.SameLine(), 33 | w.Button("No", t.doNo). 34 | Style(style.ButtonRed{}), 35 | w.Custom(func() { 36 | if t.ActionCancel != nil { 37 | w.Layout{ 38 | w.SameLine(), 39 | w.Button("Cancel", t.doCancel), 40 | }.Build() 41 | } 42 | }), 43 | }.Build() 44 | } 45 | 46 | func (t TypeConfirmation) doYes() { 47 | if t.ActionYes != nil { 48 | t.ActionYes() 49 | } 50 | imgui.CloseCurrentPopup() 51 | } 52 | 53 | func (t TypeConfirmation) doNo() { 54 | if t.ActionNo != nil { 55 | t.ActionNo() 56 | } 57 | imgui.CloseCurrentPopup() 58 | } 59 | 60 | func (t TypeConfirmation) doCancel() { 61 | if t.ActionCancel != nil { 62 | t.ActionCancel() 63 | } 64 | imgui.CloseCurrentPopup() 65 | } 66 | -------------------------------------------------------------------------------- /internal/app/ui/dialog/custom.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | w "sdmm/internal/imguiext/widget" 5 | ) 6 | 7 | type TypeCustom struct { 8 | Title string 9 | Layout w.Layout 10 | CloseButton bool 11 | } 12 | 13 | func (t TypeCustom) Name() string { 14 | return t.Title 15 | } 16 | 17 | func (t TypeCustom) Process() { 18 | t.Layout.Build() 19 | } 20 | 21 | func (t TypeCustom) HasCloseButton() bool { 22 | return t.CloseButton 23 | } 24 | -------------------------------------------------------------------------------- /internal/app/ui/dialog/dialog.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "github.com/SpaiR/imgui-go" 5 | "github.com/rs/zerolog/log" 6 | ) 7 | 8 | type Type interface { 9 | Name() string 10 | Process() 11 | HasCloseButton() bool 12 | } 13 | 14 | const popupFlags = imgui.WindowFlagsAlwaysAutoResize | imgui.WindowFlagsNoSavedSettings 15 | 16 | var opened []Type 17 | 18 | func Process() { 19 | var closedDialogs []Type 20 | for _, dialog := range opened { 21 | if !imgui.IsPopupOpen(dialog.Name()) { 22 | imgui.OpenPopup(dialog.Name()) 23 | } 24 | 25 | var isOpen bool 26 | if dialog.HasCloseButton() { 27 | open := true 28 | isOpen = imgui.BeginPopupModalV(dialog.Name(), &open, popupFlags) 29 | } else { 30 | isOpen = imgui.BeginPopupModalV(dialog.Name(), nil, popupFlags) 31 | } 32 | 33 | if isOpen { 34 | dialog.Process() 35 | imgui.EndPopup() 36 | } 37 | 38 | if !imgui.IsPopupOpen(dialog.Name()) { 39 | closedDialogs = append(closedDialogs, dialog) 40 | } 41 | } 42 | 43 | for _, dialog := range closedDialogs { 44 | Close(dialog) 45 | } 46 | } 47 | 48 | // Open opens the application dialog. 49 | func Open(t Type) { 50 | log.Print("opening dialog:", t.Name()) 51 | opened = append(opened, t) 52 | } 53 | 54 | // Close closed the application dialog. 55 | func Close(dialog Type) { 56 | log.Print("closing dialog:", dialog.Name()) 57 | for idx, t := range opened { 58 | if dialog.Name() == t.Name() { 59 | log.Print("dialog closed:", dialog.Name()) 60 | opened = append(opened[:idx], opened[idx+1:]...) 61 | return 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/app/ui/dialog/information.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "github.com/SpaiR/imgui-go" 5 | ) 6 | 7 | type TypeInformation struct { 8 | Title string 9 | Information string 10 | } 11 | 12 | func (t TypeInformation) Name() string { 13 | return t.Title 14 | } 15 | 16 | func (TypeInformation) HasCloseButton() bool { 17 | return false 18 | } 19 | 20 | func (t TypeInformation) Process() { 21 | imgui.Text(t.Information) 22 | imgui.Separator() 23 | if imgui.Button("OK") { 24 | imgui.CloseCurrentPopup() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/ui/dialog/simple.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type TypeSimple struct { 6 | Title string 7 | Message string 8 | } 9 | 10 | func (t TypeSimple) Name() string { 11 | return t.Title 12 | } 13 | 14 | func (t TypeSimple) Process() { 15 | imgui.Text(t.Message) 16 | } 17 | 18 | func (t TypeSimple) HasCloseButton() bool { 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /internal/app/ui/layout/config.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | const ( 6 | configName = "layout" 7 | configVersion = 1 8 | configState = 1 9 | ) 10 | 11 | type layoutConfig struct { 12 | Version uint 13 | State uint // When different with the configState const - layout will be reset. 14 | } 15 | 16 | func (layoutConfig) Name() string { 17 | return configName 18 | } 19 | 20 | func (layoutConfig) TryMigrate(_ map[string]any) (result map[string]any, migrated bool) { 21 | // do nothing. yet... 22 | return nil, migrated 23 | } 24 | 25 | func (l *Layout) loadConfig() { 26 | l.app.ConfigRegister(&layoutConfig{ 27 | Version: configVersion, 28 | State: configState, 29 | }) 30 | } 31 | 32 | func (l *Layout) config() *layoutConfig { 33 | if cfg, ok := l.app.ConfigFind(configName).(*layoutConfig); ok { 34 | return cfg 35 | } 36 | log.Fatal().Msg("can't find config") 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/app/ui/layout/lnode/lnode.go: -------------------------------------------------------------------------------- 1 | package lnode 2 | 3 | // Names for all layout nodes. 4 | const ( 5 | NameEnvironment = "Environment" 6 | NameWorkspaceArea = "Workspace Area" 7 | NamePrefabs = "Prefabs" 8 | NameSearch = "Search" 9 | NameVariables = "Variables" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/app/ui/menu/update.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "sdmm/internal/imguiext" 5 | "sdmm/internal/imguiext/icon" 6 | "sdmm/internal/imguiext/style" 7 | w "sdmm/internal/imguiext/widget" 8 | 9 | "github.com/SpaiR/imgui-go" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | var loadingDotTypes = []string{".", "..", "...", "...."} 14 | 15 | func (m *Menu) showUpdateMenu() { 16 | w.Button(icon.SystemUpdate, nil). 17 | Style(style.ButtonFrame{}). 18 | TextColor(style.ColorGreen1Lighter). 19 | Build() 20 | 21 | imguiext.SetItemHoveredTooltip("New update available!") 22 | 23 | imgui.OpenPopupOnItemClickV("update_menu", imgui.PopupFlagsMouseButtonLeft) 24 | 25 | if imgui.BeginPopup("update_menu") { 26 | imgui.TextColored(style.ColorGold, m.updateVersion) 27 | if len(m.updateDescription) > 0 { 28 | imgui.Text(m.updateDescription) 29 | } 30 | imgui.Separator() 31 | 32 | switch m.updateStatus { 33 | case upStatusAvailable: 34 | m.showUpdateLayout() 35 | case upStatusUpdating: 36 | dotType := loadingDotTypes[(int(imgui.Time()/0.25) & 3)] 37 | imgui.Text("Updating " + dotType) 38 | case upStatusUpdated: 39 | w.Button("Restart to Apply", m.app.DoRestart).Build() 40 | case upStatusError: 41 | imgui.TextColored(style.ColorRed, "Something went wrong.\nPlease try again later.") 42 | imgui.Separator() 43 | m.showUpdateLayout() 44 | } 45 | 46 | imgui.EndPopup() 47 | } 48 | } 49 | 50 | func (m *Menu) showUpdateLayout() { 51 | w.Layout{ 52 | w.Button("Update", m.app.DoSelfUpdate). 53 | Style(style.ButtonGreen{}), 54 | w.SameLine(), 55 | w.Button("Hide", m.doHideUpdateButton), 56 | w.SameLine(), 57 | w.Button("Ignore", m.doIgnoreUpdate). 58 | Style(style.ButtonRed{}), 59 | }.Build() 60 | } 61 | 62 | func (m *Menu) doHideUpdateButton() { 63 | log.Print("do hide update") 64 | m.updateStatus = upStatusNone 65 | } 66 | 67 | func (m *Menu) doIgnoreUpdate() { 68 | log.Print("do ignore update") 69 | m.doHideUpdateButton() 70 | m.app.DoIgnoreUpdate() 71 | } 72 | -------------------------------------------------------------------------------- /internal/app/ui/shortcut/holder.go: -------------------------------------------------------------------------------- 1 | package shortcut 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | var shortcutId uint64 6 | 7 | type Shortcuts struct { 8 | shortcuts []*Shortcut 9 | } 10 | 11 | func (s *Shortcuts) Add(shortcut Shortcut) { 12 | shortcut.id = shortcutId 13 | log.Print("adding shortcut to shortcuts:", shortcut) 14 | pShortcut := &shortcut 15 | s.shortcuts = append(s.shortcuts, pShortcut) 16 | add(pShortcut) 17 | shortcutId++ 18 | } 19 | 20 | func (s *Shortcuts) Visible() bool { 21 | for _, shortcut := range s.shortcuts { 22 | if !shortcut.IsVisible { 23 | return false 24 | } 25 | } 26 | return true 27 | } 28 | 29 | func (s *Shortcuts) SetVisible(visible bool) { 30 | for _, shortcut := range s.shortcuts { 31 | shortcut.IsVisible = visible 32 | } 33 | } 34 | 35 | func (s *Shortcuts) Dispose() { 36 | log.Print("disposing shortcuts...") 37 | for _, shortcut := range s.shortcuts { 38 | remove(shortcut) 39 | } 40 | s.shortcuts = nil 41 | log.Print("shortcuts disposed") 42 | } 43 | -------------------------------------------------------------------------------- /internal/app/update.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "runtime" 5 | 6 | "sdmm/internal/app/selfupdate" 7 | "sdmm/internal/env" 8 | "sdmm/internal/req" 9 | "sdmm/internal/util/slice" 10 | 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | var remoteManifest selfupdate.Manifest 15 | 16 | func (a *app) checkForUpdates() { 17 | a.checkForUpdatesV(false) 18 | } 19 | 20 | func (a *app) checkForUpdatesV(forceAvailable bool) { 21 | log.Print("checking for self updates...") 22 | 23 | manifest, err := selfupdate.FetchRemoteManifest() 24 | if err != nil { 25 | log.Printf("unable to fetch remote manifest: %v", err) 26 | return 27 | } 28 | 29 | remoteManifest = manifest 30 | 31 | if manifest.Version == env.Version { 32 | log.Print("application is up to date!") 33 | return 34 | } 35 | if slice.StrContains(a.config().UpdateIgnore, manifest.Version) && !forceAvailable { 36 | log.Print("ignoring update:", manifest.Version) 37 | return 38 | } 39 | 40 | log.Print("new update available:", manifest.Version) 41 | 42 | a.menu.SetUpdateAvailable(manifest.Version, manifest.Description) 43 | 44 | // Do force update only if we're using a concrete editor version. 45 | //goland:noinspection GoBoolExpressions 46 | if a.Prefs().Application.AutoUpdate && env.Version != env.Undefined { 47 | a.selfUpdate() 48 | } 49 | } 50 | 51 | func (a *app) selfUpdate() { 52 | a.menu.SetUpdating() 53 | 54 | var updateDownloadLink string 55 | 56 | switch runtime.GOOS { 57 | case "windows": 58 | updateDownloadLink = remoteManifest.DownloadLinks.Windows 59 | case "linux": 60 | updateDownloadLink = remoteManifest.DownloadLinks.Linux 61 | case "darwin": 62 | updateDownloadLink = remoteManifest.DownloadLinks.MacOS 63 | } 64 | 65 | log.Print("updating with:", updateDownloadLink) 66 | 67 | go func() { 68 | latestUpdate, err := req.Get(updateDownloadLink) 69 | if err != nil { 70 | log.Print("unable to get latest update:", err) 71 | a.menu.SetUpdateError() 72 | return 73 | } 74 | 75 | if err = selfupdate.Update(latestUpdate); err != nil { 76 | log.Print("unable to complete self update:", err) 77 | a.menu.SetUpdateError() 78 | return 79 | } 80 | 81 | a.menu.SetUpdated() 82 | 83 | log.Print("self update completed successfully!") 84 | }() 85 | } 86 | -------------------------------------------------------------------------------- /internal/app/window/fonts.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "sdmm/internal/imguiext/icon" 5 | "sdmm/internal/rsc" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | ) 9 | 10 | const ( 11 | fontSizeH1 = 32 12 | fontSizeH2 = 24 13 | fontSizeH3 = 19 14 | fontSizeH4 = 16 15 | ) 16 | 17 | var ( 18 | FontDefault imgui.Font 19 | 20 | FontH1 imgui.Font 21 | FontH2 imgui.Font 22 | FontH3 imgui.Font 23 | ) 24 | 25 | func configureFonts() { 26 | fontConfig := imgui.NewFontConfig() 27 | defer fontConfig.Delete() 28 | 29 | fontAtlas := imgui.CurrentIO().Fonts() 30 | fontAtlas.Clear() 31 | 32 | FontDefault = createFont(fontSizeH4, fontAtlas, fontConfig) 33 | 34 | FontH1 = createFont(fontSizeH1, fontAtlas, fontConfig) 35 | FontH2 = createFont(fontSizeH2, fontAtlas, fontConfig) 36 | FontH3 = createFont(fontSizeH3, fontAtlas, fontConfig) 37 | 38 | imgui.CurrentIO().SetFontDefault(FontDefault) 39 | } 40 | 41 | func createFont(size float32, atlas imgui.FontAtlas, config imgui.FontConfig) (font imgui.Font) { 42 | fontSize := size * pointSize 43 | 44 | font = atlas.AddFontFromMemoryTTFV( 45 | rsc.FontTTF(), 46 | fontSize, 47 | config, 48 | atlas.GlyphRangesCyrillic(), 49 | ) 50 | 51 | config.SetMergeMode(true) 52 | config.SetPixelSnapH(true) 53 | config.SetGlyphOffsetY(2) 54 | config.SetGlyphMaxAdvanceX(fontSize) 55 | 56 | glyphsBuilder := imgui.GlyphRangesBuilder{} 57 | glyphsBuilder.Add(icon.RangeMin, icon.RangeMax) 58 | 59 | atlas.AddFontFromMemoryTTFV( 60 | rsc.FontIconsTTF(), 61 | fontSize, 62 | config, 63 | glyphsBuilder.Build().GlyphRanges, 64 | ) 65 | 66 | config.SetMergeMode(false) 67 | 68 | return font 69 | } 70 | -------------------------------------------------------------------------------- /internal/app/window/process.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "time" 5 | 6 | "sdmm/internal/platform" 7 | 8 | "github.com/SpaiR/imgui-go" 9 | "github.com/go-gl/gl/v3.3-core/gl" 10 | "github.com/go-gl/glfw/v3.3/glfw" 11 | ) 12 | 13 | var ticker = newTicker(60) 14 | 15 | func newTicker(fps int) *time.Ticker { 16 | return time.NewTicker(time.Second / time.Duration(fps)) 17 | } 18 | 19 | func (w *Window) Process() { 20 | for !w.application.IsClosed() { 21 | // Override window closing behaviour to enforce our checks. 22 | if w.handle.ShouldClose() { 23 | w.application.CloseCheck() 24 | w.handle.SetShouldClose(false) 25 | } 26 | w.runFrame() 27 | <-ticker.C 28 | } 29 | } 30 | 31 | func (w *Window) runFrame() { 32 | w.startFrame() 33 | w.application.Process() 34 | w.endFrame() 35 | w.application.PostProcess() 36 | } 37 | 38 | func (w *Window) startFrame() { 39 | gl.Clear(gl.COLOR_BUFFER_BIT) 40 | platform.NewImGuiGLFWFrame() 41 | imgui.NewFrame() 42 | runLaterJobs() 43 | runRepeatJobs() 44 | } 45 | 46 | func runLaterJobs() { 47 | for _, job := range laterJobs { 48 | job() 49 | } 50 | laterJobs = nil 51 | } 52 | 53 | func runRepeatJobs() { 54 | for _, job := range repeatJobs { 55 | job() 56 | } 57 | } 58 | 59 | func (w *Window) endFrame() { 60 | imgui.Render() 61 | platform.Render(imgui.RenderedDrawData()) 62 | w.handle.SwapBuffers() 63 | glfw.PollEvents() 64 | } 65 | -------------------------------------------------------------------------------- /internal/app/window/restart.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "runtime" 7 | "syscall" 8 | 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | // Path to the current exe file. Required to do a correct restart after self update applied. 13 | var selfExecutableName string 14 | 15 | func init() { 16 | executableName, err := os.Executable() 17 | if err != nil { 18 | panic("unable to get executable name: " + err.Error()) 19 | } 20 | selfExecutableName = executableName 21 | } 22 | 23 | func Restart() { 24 | if err := restartSelf(); err != nil { 25 | log.Print("unable to restart gracefully:", err) 26 | panic("unable to restart gracefully: " + err.Error()) 27 | } 28 | } 29 | 30 | func restartSelf() error { 31 | args := os.Args 32 | env := os.Environ() 33 | 34 | // Windows requires custom restart logic. 35 | if runtime.GOOS == "windows" { 36 | cmd := exec.Command(selfExecutableName, args[1:]...) 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | cmd.Stdin = os.Stdin 40 | cmd.Env = env 41 | err := cmd.Run() 42 | if err == nil { 43 | os.Exit(0) 44 | } 45 | return err 46 | } 47 | 48 | return syscall.Exec(selfExecutableName, args, env) 49 | } 50 | -------------------------------------------------------------------------------- /internal/app/window/util.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | func (w *Window) AddMouseChangeCallback(cb func(uint, uint)) (callbackId int) { 6 | id := w.mouseChangeCallbackId 7 | w.mouseChangeCallbacks[id] = cb 8 | w.mouseChangeCallbackId++ 9 | log.Print("mouse change callback added:", id) 10 | return id 11 | } 12 | 13 | func (w *Window) RemoveMouseChangeCallback(id int) { 14 | delete(w.mouseChangeCallbacks, id) 15 | log.Print("mouse change callback deleted:", id) 16 | } 17 | 18 | var laterJobs []func() 19 | 20 | // RunLater queues provided a job to be run in the next frame. 21 | func RunLater(job func()) { 22 | laterJobs = append(laterJobs, job) 23 | } 24 | 25 | var repeatJobs []func() 26 | 27 | // RunRepeat stores provided a job in a separate slice to run it in all other frames. 28 | // Unlike the RunLater job will be executed all the time until the application shut down. 29 | func RunRepeat(job func()) { 30 | repeatJobs = append(repeatJobs, job) 31 | } 32 | -------------------------------------------------------------------------------- /internal/dmapi/dm/dirs.go: -------------------------------------------------------------------------------- 1 | package dm 2 | 3 | const ( 4 | DirNorth = 1 5 | DirSouth = 2 6 | DirEast = 4 7 | DirWest = 8 8 | 9 | DirNortheast = 5 10 | DirNorthwest = 9 11 | DirSoutheast = 6 12 | DirSouthwest = 10 13 | 14 | DirDefault = DirSouth 15 | ) 16 | -------------------------------------------------------------------------------- /internal/dmapi/dm/path.go: -------------------------------------------------------------------------------- 1 | package dm 2 | 3 | import "strings" 4 | 5 | // IsPath returns true if the orig path is the same type of the provided. 6 | func IsPath(orig, path string) bool { 7 | return strings.HasPrefix(orig, path) 8 | } 9 | 10 | // IsPathBaseSame returns true if both provided paths has the same base. 11 | func IsPathBaseSame(p1, p2 string) bool { 12 | return PathBase(p1) == PathBase(p2) 13 | } 14 | 15 | // PathWeight let us sort the content by the path weight. Basically: /obj->/turf->/area. 16 | func PathWeight(p string) int { 17 | if IsPath(p, "/area") { 18 | return 3 19 | } 20 | if IsPath(p, "/turf") { 21 | return 2 22 | } 23 | return 1 24 | } 25 | 26 | // PathBase returns the base of the provided path. 27 | // Example: /obj/item/weapon -> /obj. 28 | func PathBase(p string) string { 29 | separatorIdx := strings.Index(p[1:], "/") + 1 30 | return p[:separatorIdx] 31 | } 32 | 33 | // PathLast returns the last part of the path (basically, a name of the type). 34 | // Example: /obj/item/weapon -> weapon. 35 | func PathLast(p string) string { 36 | return p[strings.LastIndex(p, "/")+1:] 37 | } 38 | 39 | // IsMovable returns true if the provided path is a "movable" type. 40 | func IsMovable(path string) bool { 41 | return IsPath(path, "/obj") || IsPath(path, "/mob") || IsPath(path, "/atom/movable") 42 | } 43 | -------------------------------------------------------------------------------- /internal/dmapi/dm/paths_filter.go: -------------------------------------------------------------------------------- 1 | package dm 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | type PathsFilter struct { 10 | findDirectChildren func(string) []string 11 | filteredPaths map[string]bool 12 | } 13 | 14 | func NewPathsFilter(findDirectChildren func(string) []string) *PathsFilter { 15 | return &PathsFilter{ 16 | findDirectChildren: findDirectChildren, 17 | filteredPaths: make(map[string]bool), 18 | } 19 | } 20 | 21 | func NewPathsFilterEmpty() *PathsFilter { 22 | return NewPathsFilter(func(string) []string { 23 | return nil 24 | }) 25 | } 26 | 27 | func (p *PathsFilter) Clear() { 28 | p.filteredPaths = make(map[string]bool) 29 | } 30 | 31 | func (p *PathsFilter) Copy() PathsFilter { 32 | filteredPaths := make(map[string]bool, len(p.filteredPaths)) 33 | for path := range p.filteredPaths { 34 | filteredPaths[path] = true 35 | } 36 | return PathsFilter{ 37 | p.findDirectChildren, 38 | filteredPaths, 39 | } 40 | } 41 | 42 | func (p *PathsFilter) IsHiddenPath(path string) bool { 43 | return p.filteredPaths[path] 44 | } 45 | 46 | func (p *PathsFilter) IsVisiblePath(path string) bool { 47 | return !p.IsHiddenPath(path) 48 | } 49 | 50 | func (p *PathsFilter) HasHiddenChildPath(path string) bool { 51 | for filteredPath := range p.filteredPaths { 52 | if strings.HasPrefix(filteredPath, path) { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | func (p *PathsFilter) TogglePath(path string) { 60 | p.togglePath(path, p.IsVisiblePath(path)) 61 | log.Printf("toggle [%s] path: [%t]", path, p.IsVisiblePath(path)) 62 | } 63 | 64 | func (p *PathsFilter) togglePath(path string, isFilteredOut bool) { 65 | for _, directChild := range p.findDirectChildren(path) { 66 | p.togglePath(directChild, isFilteredOut) 67 | } 68 | if isFilteredOut { 69 | p.filteredPaths[path] = true 70 | } else { 71 | delete(p.filteredPaths, path) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/dmapi/dmenv/object.go: -------------------------------------------------------------------------------- 1 | package dmenv 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmvars" 5 | "sdmm/third_party/sdmmparser" 6 | ) 7 | 8 | type VarFlags struct { 9 | Tmp bool 10 | Const bool 11 | Static bool 12 | } 13 | 14 | func (vf VarFlags) Any() bool { 15 | return vf.Tmp || vf.Const || vf.Static 16 | } 17 | 18 | func (vf VarFlags) ReadOnly() bool { 19 | return vf.Const || vf.Static 20 | } 21 | 22 | type Object struct { 23 | env *Dme 24 | parent *Object 25 | 26 | Vars *dmvars.Variables 27 | VarFlags map[string]VarFlags 28 | Path string 29 | DirectChildren []string 30 | Location sdmmparser.Location 31 | } 32 | 33 | func (o *Object) Parent() *Object { 34 | return o.parent 35 | } 36 | 37 | func (o *Object) Flags(varName string) VarFlags { 38 | for o != nil { 39 | if value, ok := o.VarFlags[varName]; ok { 40 | return value 41 | } 42 | o = o.parent 43 | } 44 | return VarFlags{} 45 | } 46 | -------------------------------------------------------------------------------- /internal/dmapi/dmicon/cache.go: -------------------------------------------------------------------------------- 1 | package dmicon 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "sdmm/internal/dmapi/dm" 8 | 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | var Cache = &IconsCache{icons: make(map[string]*Dmi)} 13 | 14 | type IconsCache struct { 15 | rootDirPath string 16 | icons map[string]*Dmi 17 | } 18 | 19 | func (i *IconsCache) Free() { 20 | for _, dmi := range i.icons { 21 | dmi.free() 22 | } 23 | log.Printf("cache free; [%d] icons disposed", len(i.icons)) 24 | i.rootDirPath = "" 25 | i.icons = make(map[string]*Dmi) 26 | } 27 | 28 | func (i *IconsCache) SetRootDirPath(rootDirPath string) { 29 | i.rootDirPath = rootDirPath 30 | log.Print("cache root dir:", rootDirPath) 31 | } 32 | 33 | func (i *IconsCache) Get(icon string) (*Dmi, error) { 34 | if len(icon) == 0 { 35 | return nil, errors.New("dmi icon is empty") 36 | } 37 | 38 | if dmi, ok := i.icons[icon]; ok { 39 | if dmi == nil { 40 | return nil, fmt.Errorf("dmi [%s] is nil", icon) 41 | } 42 | return dmi, nil 43 | } 44 | 45 | dmi, err := New(i.rootDirPath + "/" + icon) 46 | i.icons[icon] = dmi 47 | return dmi, err 48 | } 49 | 50 | func (i *IconsCache) GetState(icon, state string) (*State, error) { 51 | dmi, err := i.Get(icon) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return dmi.State(state) 56 | } 57 | 58 | func (i *IconsCache) GetSpriteV(icon, state string, dir int) (*Sprite, error) { 59 | dmiState, err := i.GetState(icon, state) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return dmiState.SpriteV(dir), nil 64 | } 65 | 66 | func (i *IconsCache) GetSprite(icon, state string) (*Sprite, error) { 67 | return i.GetSpriteV(icon, state, dm.DirDefault) 68 | } 69 | 70 | func (i *IconsCache) GetSpriteOrPlaceholder(icon, state string) *Sprite { 71 | return i.GetSpriteOrPlaceholderV(icon, state, dm.DirDefault) 72 | } 73 | 74 | func (i *IconsCache) GetSpriteOrPlaceholderV(icon, state string, dir int) *Sprite { 75 | if s, err := i.GetSpriteV(icon, state, dir); err == nil { 76 | return s 77 | } 78 | return SpritePlaceholder() 79 | } 80 | -------------------------------------------------------------------------------- /internal/dmapi/dmicon/editor.go: -------------------------------------------------------------------------------- 1 | package dmicon 2 | 3 | import ( 4 | "sdmm/internal/platform" 5 | "sdmm/internal/rsc" 6 | ) 7 | 8 | var ( 9 | spritePlaceholder *Sprite 10 | whiteRect *Sprite 11 | ) 12 | 13 | func initEditorSprites() { 14 | atlas := rsc.EditorTextureAtlas() 15 | img := atlas.RGBA() 16 | 17 | dmi := &Dmi{ 18 | IconWidth: 32, 19 | IconHeight: 32, 20 | TextureWidth: atlas.Width, 21 | TextureHeight: atlas.Height, 22 | Cols: 2, 23 | Rows: 1, 24 | Image: img, 25 | Texture: platform.CreateTexture(img), 26 | } 27 | 28 | spritePlaceholder = newDmiSprite(dmi, 0) 29 | whiteRect = newDmiSprite(dmi, 1) 30 | } 31 | 32 | func SpritePlaceholder() *Sprite { 33 | if spritePlaceholder == nil { 34 | initEditorSprites() 35 | } 36 | return spritePlaceholder 37 | } 38 | 39 | func WhiteRect() *Sprite { 40 | if whiteRect == nil { 41 | initEditorSprites() 42 | } 43 | return whiteRect 44 | } 45 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmap.go: -------------------------------------------------------------------------------- 1 | package dmmap 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmenv" 5 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 6 | 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | var ( 11 | WorldIconSize int 12 | 13 | /* 14 | Tiles should have at least one area and one turf. 15 | Those vars will store them to ensure that the tile has a proper content. 16 | */ 17 | BaseArea *dmmprefab.Prefab 18 | BaseTurf *dmmprefab.Prefab 19 | 20 | environment *dmenv.Dme 21 | ) 22 | 23 | func Init(dme *dmenv.Dme) { 24 | environment = dme 25 | 26 | WorldIconSize = dme.Objects["/world"].Vars.IntV("icon_size", 32) 27 | 28 | baseAreaPath, _ := dme.Objects["/world"].Vars.Value("area") 29 | baseTurfPath, _ := dme.Objects["/world"].Vars.Value("turf") 30 | BaseArea = PrefabStorage.Initial(baseAreaPath) 31 | BaseTurf = PrefabStorage.Initial(baseTurfPath) 32 | 33 | log.Print("initialized with:", dme.RootFile) 34 | log.Print("base area:", baseAreaPath) 35 | log.Print("base turf:", baseTurfPath) 36 | } 37 | 38 | func Free() { 39 | environment = nil 40 | WorldIconSize = 0 41 | BaseArea = nil 42 | BaseTurf = nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmdata/dmmdata.go: -------------------------------------------------------------------------------- 1 | package dmmdata 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | 8 | "sdmm/internal/util" 9 | ) 10 | 11 | type ( 12 | DataDictionary map[Key]Prefabs 13 | DataGrid map[util.Point]Key 14 | ) 15 | 16 | // DmmData stores raw information about the map. Mostly needed to for parsing and saving. 17 | type DmmData struct { 18 | Filepath string 19 | 20 | IsTgm bool 21 | LineBreak string 22 | 23 | KeyLength int 24 | MaxX, MaxY, MaxZ int 25 | 26 | Dictionary DataDictionary 27 | Grid DataGrid 28 | } 29 | 30 | func (d DmmData) Save() { 31 | if d.IsTgm { 32 | d.SaveTGM(d.Filepath) 33 | } else { 34 | d.SaveDM(d.Filepath) 35 | } 36 | } 37 | 38 | func (d DmmData) Keys() []Key { 39 | keys := make([]Key, 0, len(d.Dictionary)) 40 | for key := range d.Dictionary { 41 | keys = append(keys, key) 42 | } 43 | 44 | sort.Slice(keys, func(i, j int) bool { 45 | return keys[i].ToNum() < keys[j].ToNum() 46 | }) 47 | 48 | return keys 49 | } 50 | 51 | func (d DmmData) String() string { 52 | var winLineBreak bool 53 | if d.LineBreak == "\r\n" { 54 | winLineBreak = true 55 | } 56 | return fmt.Sprintf( 57 | "Filepath: %s, IsTgm: %t, WinLineBreak: %v, KeyLength: %d, MaxX: %d, MaxY: %d, MaxZ: %d", 58 | d.Filepath, d.IsTgm, winLineBreak, d.KeyLength, d.MaxX, d.MaxY, d.MaxZ) 59 | } 60 | 61 | func New(path string) (*DmmData, error) { 62 | file, err := os.Open(path) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer file.Close() 67 | return parse(file) 68 | } 69 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmdata/dmmprefab/prefab.go: -------------------------------------------------------------------------------- 1 | package dmmprefab 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmvars" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | const ( 9 | IdNone = 0 10 | IdStage = 1 // Prefabs with this ID are temporal by their nature. 11 | ) 12 | 13 | type Prefab struct { 14 | id uint64 15 | path string 16 | vars *dmvars.Variables 17 | } 18 | 19 | func New(id uint64, path string, vars *dmvars.Variables) *Prefab { 20 | return &Prefab{id, path, vars} 21 | } 22 | 23 | func (p Prefab) Id() uint64 { 24 | if p.id == IdNone { 25 | p.id = Id(p.path, p.vars) 26 | } 27 | return p.id 28 | } 29 | 30 | func (p Prefab) Path() string { 31 | return p.path 32 | } 33 | 34 | func (p Prefab) Vars() *dmvars.Variables { 35 | return p.vars 36 | } 37 | 38 | // Stage returns a copy of the prefab with the ID equals to IdStage. Staged prefabs are temporal. 39 | // They are needed when creating/modifying existing prefab, without persisting of the temporal object. 40 | func (p Prefab) Stage() Prefab { 41 | return Prefab{ 42 | id: IdStage, 43 | path: p.path, 44 | vars: p.vars, 45 | } 46 | } 47 | 48 | func Id(path string, vars *dmvars.Variables) uint64 { 49 | snap := path 50 | if vars != nil { 51 | for _, name := range vars.Iterate() { 52 | if value, ok := vars.Value(name); ok { 53 | snap += name + value 54 | } else { 55 | snap += name 56 | } 57 | } 58 | } 59 | return util.Djb2(snap) 60 | } 61 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmdata/key.go: -------------------------------------------------------------------------------- 1 | package dmmdata 2 | 3 | type Key string 4 | 5 | const base = 52 6 | 7 | var base52r map[rune]int 8 | 9 | func init() { 10 | var base52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 11 | base52r = make(map[rune]int, len(base52)) 12 | for idx, c := range base52 { 13 | base52r[c] = idx 14 | } 15 | } 16 | 17 | func (k Key) ToNum() int { 18 | num := 0 19 | for _, c := range k { 20 | num = base*num + base52r[c] 21 | } 22 | return num 23 | } 24 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmdata/prefabs.go: -------------------------------------------------------------------------------- 1 | package dmmdata 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "strings" 7 | 8 | "sdmm/internal/dmapi/dm" 9 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 10 | "sdmm/internal/util" 11 | ) 12 | 13 | type Prefabs []*dmmprefab.Prefab 14 | 15 | func (p Prefabs) Copy() Prefabs { 16 | cpy := make(Prefabs, len(p)) 17 | copy(cpy, p) 18 | return cpy 19 | } 20 | 21 | func (p Prefabs) Equals(prefabs Prefabs) bool { 22 | if len(p) != len(prefabs) { 23 | return false 24 | } 25 | 26 | for idx, prefab := range p { 27 | if prefab.Id() != prefabs[idx].Id() { 28 | return false 29 | } 30 | } 31 | 32 | return true 33 | } 34 | 35 | func (p Prefabs) Hash() uint64 { 36 | sb := strings.Builder{} 37 | for _, prefab := range p { 38 | sb.WriteString(strconv.FormatUint(prefab.Id(), 10)) 39 | } 40 | return util.Djb2(sb.String()) 41 | } 42 | 43 | func (p Prefabs) Sorted() Prefabs { 44 | sorted := p.Copy() 45 | sort.SliceStable(sorted, func(i, j int) bool { 46 | return dm.PathWeight(sorted[i].Path()) < dm.PathWeight(sorted[j].Path()) 47 | }) 48 | return sorted 49 | } 50 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmdata/save_dm.go: -------------------------------------------------------------------------------- 1 | package dmmdata 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "sdmm/internal/util" 10 | 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | // SaveDM writes DmmData in DM format to a file with the provided path. 15 | func (d DmmData) SaveDM(path string) { 16 | log.Print("saving dmm data in format...") 17 | 18 | f, err := os.Create(path) 19 | if err != nil { 20 | log.Printf("unable to save as [%s]: %v", d, err) 21 | return 22 | } 23 | defer f.Close() 24 | 25 | w := bufio.NewWriter(f) 26 | write := func(str string) { 27 | _, _ = w.WriteString(str) 28 | } 29 | 30 | log.Print("writing prefabs...") 31 | 32 | for _, key := range d.Keys() { 33 | write(toDMStr(key, d.Dictionary[key])) 34 | write(d.LineBreak) 35 | } 36 | 37 | log.Print("writing grid...") 38 | 39 | for z := 1; z <= d.MaxZ; z++ { 40 | write(d.LineBreak) 41 | write(fmt.Sprintf("(1,1,%d) = {\"", z)) 42 | write(d.LineBreak) 43 | 44 | for y := d.MaxY; y >= 1; y-- { 45 | for x := 1; x <= d.MaxX; x++ { 46 | write(string(d.Grid[util.Point{X: x, Y: y, Z: z}])) 47 | } 48 | write(d.LineBreak) 49 | } 50 | 51 | write("\"}") 52 | } 53 | 54 | write(d.LineBreak) 55 | 56 | if err = w.Flush(); err != nil { 57 | log.Printf("unable to write to [%s]: %v", path, err) 58 | } 59 | 60 | log.Printf("[%s] saved in format to: %s", d, path) 61 | } 62 | 63 | func toDMStr(key Key, prefabs Prefabs) string { 64 | sb := strings.Builder{} 65 | 66 | sb.WriteString(fmt.Sprintf("\"%s\" = (", key)) 67 | 68 | for idx, prefab := range prefabs { 69 | sb.WriteString(prefab.Path()) 70 | 71 | if prefab.Vars().Len() > 0 { 72 | sb.WriteString("{") 73 | 74 | for idx, varName := range prefab.Vars().Iterate() { 75 | varValue, _ := prefab.Vars().Value(varName) 76 | 77 | sb.WriteString(varName) 78 | sb.WriteString(" = ") 79 | sb.WriteString(varValue) 80 | 81 | if idx != prefab.Vars().Len()-1 { 82 | sb.WriteString("; ") 83 | } 84 | } 85 | 86 | sb.WriteString("}") 87 | } 88 | 89 | if idx != len(prefabs)-1 { 90 | sb.WriteString(",") 91 | } 92 | } 93 | 94 | sb.WriteString(")") 95 | 96 | return sb.String() 97 | } 98 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmmdata/save_tgm.go: -------------------------------------------------------------------------------- 1 | package dmmdata 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "sdmm/internal/util" 10 | 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | // SaveTGM writes DmmData in TGM format to a file with the provided path. 15 | func (d DmmData) SaveTGM(path string) { 16 | log.Print("saving dmm data in [TGM] format...") 17 | 18 | f, err := os.Create(path) 19 | if err != nil { 20 | log.Printf("unable to save as [TGM] [%s]: %v", d, err) 21 | return 22 | } 23 | defer f.Close() 24 | 25 | w := bufio.NewWriter(f) 26 | writeln := func(str ...string) { 27 | for _, s := range str { 28 | _, _ = w.WriteString(s) 29 | } 30 | _, _ = w.WriteString(d.LineBreak) 31 | } 32 | 33 | // Write TGM header 34 | // yeah, yeah, dmm2tgm.py, sure... 35 | writeln("//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE") 36 | 37 | log.Print("writing prefabs...") 38 | 39 | for _, key := range d.Keys() { 40 | writeln(toTGMStr(key, d.Dictionary[key], d.LineBreak)) 41 | } 42 | 43 | log.Print("writing grid...") 44 | 45 | for z := 1; z <= d.MaxZ; z++ { 46 | writeln() 47 | 48 | for x := 1; x <= d.MaxX; x++ { 49 | writeln(fmt.Sprintf("(%d,1,%d) = {\"", x, z)) 50 | 51 | for y := d.MaxY; y >= 1; y-- { 52 | writeln(string(d.Grid[util.Point{X: x, Y: y, Z: z}])) 53 | } 54 | 55 | writeln("\"}") 56 | } 57 | } 58 | 59 | if err = w.Flush(); err != nil { 60 | log.Printf("unable to write to [%s]: %v", path, err) 61 | } 62 | 63 | log.Printf("[%s] saved in [TGM] format to: %s", d, path) 64 | } 65 | 66 | func toTGMStr(key Key, content Prefabs, lineBreak string) string { 67 | sb := strings.Builder{} 68 | 69 | sb.WriteString(fmt.Sprintf("\"%s\" = (", key)) 70 | sb.WriteString(lineBreak) 71 | 72 | for idx, prefab := range content { 73 | sb.WriteString(prefab.Path()) 74 | 75 | if prefab.Vars().Len() > 0 { 76 | sb.WriteString("{") 77 | sb.WriteString(lineBreak) 78 | 79 | for idx, varName := range prefab.Vars().Iterate() { 80 | varValue, _ := prefab.Vars().Value(varName) 81 | 82 | sb.WriteString("\t") 83 | sb.WriteString(varName) 84 | sb.WriteString(" = ") 85 | sb.WriteString(varValue) 86 | 87 | if idx != prefab.Vars().Len()-1 { 88 | sb.WriteString(";") 89 | } 90 | 91 | sb.WriteString(lineBreak) 92 | } 93 | 94 | sb.WriteString("\t}") 95 | } 96 | 97 | if idx != len(content)-1 { 98 | sb.WriteString(",") 99 | sb.WriteString(lineBreak) 100 | } 101 | } 102 | 103 | sb.WriteString(")") 104 | 105 | return sb.String() 106 | } 107 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/dmminstance/instance.go: -------------------------------------------------------------------------------- 1 | package dmminstance 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 5 | "sdmm/internal/util" 6 | ) 7 | 8 | var id uint64 9 | 10 | type Instance struct { 11 | id uint64 12 | coord util.Point 13 | prefab *dmmprefab.Prefab 14 | } 15 | 16 | func (i *Instance) SetPrefab(prefab *dmmprefab.Prefab) { 17 | i.prefab = prefab 18 | } 19 | 20 | func (i Instance) Copy() Instance { 21 | return Instance{ 22 | id: i.id, 23 | coord: i.coord, 24 | prefab: i.prefab, 25 | } 26 | } 27 | 28 | func (i Instance) Id() uint64 { 29 | return i.id 30 | } 31 | 32 | func (i Instance) Coord() util.Point { 33 | return i.coord 34 | } 35 | 36 | func (i Instance) Prefab() *dmmprefab.Prefab { 37 | return i.prefab 38 | } 39 | 40 | func New(coord util.Point, prefab *dmmprefab.Prefab) *Instance { 41 | id++ 42 | return &Instance{ 43 | id, 44 | coord, 45 | prefab, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/instances.go: -------------------------------------------------------------------------------- 1 | package dmmap 2 | 3 | import ( 4 | "sort" 5 | 6 | "sdmm/internal/dmapi/dm" 7 | "sdmm/internal/dmapi/dmmap/dmmdata" 8 | "sdmm/internal/dmapi/dmmap/dmminstance" 9 | "sdmm/internal/util" 10 | ) 11 | 12 | type Instances []*dmminstance.Instance 13 | 14 | func InstancesFromPrefabs(coord util.Point, prefabs dmmdata.Prefabs) Instances { 15 | instances := make(Instances, 0, len(prefabs)) 16 | for _, prefab := range prefabs { 17 | instances = append(instances, dmminstance.New(coord, prefab)) 18 | } 19 | return instances 20 | } 21 | 22 | func (i Instances) PrefabsEquals(instances Instances) bool { 23 | if len(i) != len(instances) { 24 | return false 25 | } 26 | 27 | for idx, instance := range i { 28 | if instance.Prefab().Id() != instances[idx].Prefab().Id() { 29 | return false 30 | } 31 | } 32 | 33 | return true 34 | } 35 | 36 | func (i Instances) Prefabs() dmmdata.Prefabs { 37 | prefabs := make(dmmdata.Prefabs, 0, len(i)) 38 | for _, instance := range i { 39 | prefabs = append(prefabs, instance.Prefab()) 40 | } 41 | return prefabs 42 | } 43 | 44 | func (i Instances) Copy() Instances { 45 | cpy := make(Instances, len(i)) 46 | copy(cpy, i) 47 | return cpy 48 | } 49 | 50 | func (i Instances) DeepCopy() Instances { 51 | cpy := make(Instances, 0, len(i)) 52 | for _, instance := range i { 53 | instance := instance.Copy() 54 | cpy = append(cpy, &instance) 55 | } 56 | return cpy 57 | } 58 | 59 | func (i Instances) Sorted() Instances { 60 | sorted := i.Copy() 61 | sort.SliceStable(sorted, func(i, j int) bool { 62 | return dm.PathWeight(sorted[i].Prefab().Path()) < dm.PathWeight(sorted[j].Prefab().Path()) 63 | }) 64 | return sorted 65 | } 66 | -------------------------------------------------------------------------------- /internal/dmapi/dmmap/tile.go: -------------------------------------------------------------------------------- 1 | package dmmap 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dm" 5 | "sdmm/internal/dmapi/dmmap/dmmdata" 6 | "sdmm/internal/dmapi/dmmap/dmmdata/dmmprefab" 7 | "sdmm/internal/dmapi/dmmap/dmminstance" 8 | "sdmm/internal/util" 9 | ) 10 | 11 | type Tile struct { 12 | Coord util.Point 13 | instances Instances 14 | } 15 | 16 | func (t Tile) Copy() Tile { 17 | return Tile{ 18 | t.Coord, 19 | t.instances.DeepCopy(), 20 | } 21 | } 22 | 23 | func (t *Tile) Set(instances Instances) { 24 | t.instances = instances 25 | } 26 | 27 | func (t Tile) Instances() Instances { 28 | return t.instances 29 | } 30 | 31 | func (t *Tile) InstancesSet(prefabs dmmdata.Prefabs) { 32 | t.instances = InstancesFromPrefabs(t.Coord, prefabs) 33 | } 34 | 35 | func (t *Tile) InstancesAdd(prefab *dmmprefab.Prefab) { 36 | t.instances = append(t.instances, dmminstance.New(t.Coord, prefab)) 37 | } 38 | 39 | func (t *Tile) InstancesRemoveByPath(pathToRemove string) { 40 | instances := make(Instances, 0, len(t.instances)) 41 | for _, instance := range t.instances { 42 | if !dm.IsPath(instance.Prefab().Path(), pathToRemove) { 43 | instances = append(instances, instance) 44 | } 45 | } 46 | t.instances = instances 47 | } 48 | 49 | func (t *Tile) InstancesRemoveByInstance(i *dmminstance.Instance) { 50 | for idx, instance := range t.instances { 51 | if instance.Id() == i.Id() { 52 | t.instances = append(t.instances[:idx], t.instances[idx+1:]...) 53 | return 54 | } 55 | } 56 | } 57 | 58 | // InstancesRegenerate adds missing base prefabs, if there are some of them. 59 | func (t *Tile) InstancesRegenerate() { 60 | var hasArea, hasTurf bool 61 | for _, instance := range t.instances { 62 | if dm.IsPath(instance.Prefab().Path(), "/area") { 63 | hasArea = true 64 | } else if dm.IsPath(instance.Prefab().Path(), "/turf") { 65 | hasTurf = true 66 | } 67 | } 68 | if !hasArea { 69 | t.InstancesAdd(BaseArea) 70 | } 71 | if !hasTurf { 72 | t.InstancesAdd(BaseTurf) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/dmapi/dmmclip/dmmclip.go: -------------------------------------------------------------------------------- 1 | package dmmclip 2 | 3 | import ( 4 | "sort" 5 | 6 | "sdmm/internal/dmapi/dm" 7 | "sdmm/internal/dmapi/dmmap" 8 | "sdmm/internal/dmapi/dmmap/dmmdata" 9 | "sdmm/internal/util" 10 | 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | type PasteData struct { 15 | Filter dm.PathsFilter 16 | Buffer []dmmap.Tile 17 | } 18 | 19 | // Clipboard is a global storage for tiles to provide a copy/paste experience. 20 | type Clipboard struct { 21 | pasteData PasteData 22 | } 23 | 24 | func New() *Clipboard { 25 | return &Clipboard{} 26 | } 27 | 28 | func (c *Clipboard) Free() { 29 | c.pasteData = PasteData{} 30 | log.Print("clipboard free") 31 | } 32 | 33 | func (c *Clipboard) Copy(pathsFilter *dm.PathsFilter, dmm *dmmap.Dmm, tiles []util.Point) { 34 | if len(tiles) == 0 { 35 | return 36 | } 37 | 38 | log.Printf("copy tiles to the clipboard buffer: %v", tiles) 39 | 40 | c.pasteData.Filter = pathsFilter.Copy() 41 | c.pasteData.Buffer = make([]dmmap.Tile, 0, len(tiles)) 42 | 43 | for _, pos := range tiles { 44 | if !dmm.HasTile(pos) { 45 | continue 46 | } 47 | 48 | tile := dmm.GetTile(pos).Copy() 49 | 50 | var prefabs dmmdata.Prefabs 51 | for _, instance := range tile.Instances() { 52 | if pathsFilter.IsVisiblePath(instance.Prefab().Path()) { 53 | prefabs = append(prefabs, instance.Prefab()) 54 | } 55 | } 56 | 57 | tile.InstancesSet(prefabs) 58 | 59 | c.pasteData.Buffer = append(c.pasteData.Buffer, tile) 60 | } 61 | 62 | sort.SliceStable(c.pasteData.Buffer, func(i, j int) bool { 63 | return c.pasteData.Buffer[i].Coord.Y < c.pasteData.Buffer[j].Coord.Y 64 | }) 65 | sort.SliceStable(c.pasteData.Buffer, func(i, j int) bool { 66 | return c.pasteData.Buffer[i].Coord.X < c.pasteData.Buffer[j].Coord.X 67 | }) 68 | } 69 | 70 | func (c *Clipboard) Buffer() PasteData { 71 | return c.pasteData 72 | } 73 | 74 | func (c *Clipboard) HasData() bool { 75 | return len(c.pasteData.Buffer) != 0 76 | } 77 | -------------------------------------------------------------------------------- /internal/dmapi/dmmsave/config.go: -------------------------------------------------------------------------------- 1 | package dmmsave 2 | 3 | type Format uint 4 | 5 | const ( 6 | FormatInitial Format = iota 7 | FormatTGM 8 | FormatDM 9 | ) 10 | 11 | type Config struct { 12 | Format Format 13 | 14 | SanitizeVariables bool 15 | } 16 | -------------------------------------------------------------------------------- /internal/dmapi/dmmsave/dmmsave.go: -------------------------------------------------------------------------------- 1 | package dmmsave 2 | 3 | import ( 4 | "sdmm/internal/dmapi/dmenv" 5 | 6 | "sdmm/internal/dmapi/dmmap" 7 | "sdmm/internal/util" 8 | 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | func Save(dme *dmenv.Dme, dmm *dmmap.Dmm, cfg Config) { 13 | SaveV(dme, dmm, dmm.Path.Absolute, cfg) 14 | } 15 | 16 | func SaveV(dme *dmenv.Dme, dmm *dmmap.Dmm, path string, cfg Config) { 17 | log.Printf("save started [%s]...", path) 18 | 19 | sp, err := makeSaveProcess(cfg, dme, dmm, path) 20 | if err != nil { 21 | log.Print("unable to start save process") 22 | util.ShowErrorDialog("Unable to start save process") 23 | return 24 | } 25 | 26 | if cfg.SanitizeVariables { 27 | sp.sanitizeVariables() 28 | } 29 | 30 | sp.handleReusedKeys() 31 | if err = sp.handleLocationsWithoutKeys(); err != nil { 32 | log.Print("unable to handle locations without keys:", err) 33 | util.ShowErrorDialog("Unable to save the map: " + err.Error()) 34 | return 35 | } 36 | sp.output.Save() 37 | 38 | log.Print("save finished") 39 | } 40 | -------------------------------------------------------------------------------- /internal/dmapi/dmmsave/error_code.go: -------------------------------------------------------------------------------- 1 | package dmmsave 2 | 3 | import "github.com/rs/zerolog/log" 4 | 5 | //nolint:errname 6 | type saveErrorCode int 7 | 8 | const ( 9 | errRegenerateKeys saveErrorCode = iota 10 | errKeysLimitExceeded 11 | ) 12 | 13 | func (s saveErrorCode) Error() string { 14 | switch s { 15 | case errRegenerateKeys: 16 | return "regenerate keys error" 17 | case errKeysLimitExceeded: 18 | return "keys limit exceeded error" 19 | } 20 | log.Panic().Msg("unknown error code") 21 | return "" // unreachable 22 | } 23 | -------------------------------------------------------------------------------- /internal/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | const Undefined = "undefined" 4 | 5 | var ( 6 | Title = "StrongDMM" 7 | Version = Undefined 8 | Revision = Undefined 9 | GitHub = "https://github.com/SpaiR/StrongDMM" 10 | Manifest = "https://spair.github.io/StrongDMM/manifest.json" 11 | Support = "https://ko-fi.com/spair" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/imguiext/icon/icon.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | const ( 4 | RangeMin rune = 0xe900 5 | RangeMax rune = 0xe9ff 6 | ) 7 | 8 | const ( 9 | ClipboardMultiple = "\ue900" 10 | Cog = "\ue901" 11 | WindowRestore = "\ue902" 12 | Wrench = "\ue903" 13 | FolderOpen = "\ue904" 14 | Eraser = "\ue905" 15 | EyeDropper = "\ue906" 16 | Add = "\ue907" 17 | Remove = "\ue908" 18 | AddBox = "\ue909" 19 | Clear = "\ue90a" 20 | ContentCopy = "\ue90b" 21 | ContentCut = "\ue90c" 22 | ContentPaste = "\ue90d" 23 | Redo = "\ue90e" 24 | Save = "\ue90f" 25 | Undo = "\ue910" 26 | BorderAll = "\ue911" 27 | BorderStyle = "\ue912" 28 | File = "\ue913" 29 | Eye = "\ue914" 30 | ArrowUpward = "\ue915" 31 | ArrowDownward = "\ue916" 32 | Delete = "\ue917" 33 | Help = "\ue918" 34 | Search = "\ue919" 35 | SystemUpdate = "\ue91a" 36 | FilterAlt = "\ue91b" 37 | GitHub = "\ue91c" 38 | KoFi = "\ue91d" 39 | Repeat = "\ue91e" 40 | AccessTime = "\ue91f" 41 | Shrink = "\ue98a" 42 | ) 43 | -------------------------------------------------------------------------------- /internal/imguiext/imguiext.go: -------------------------------------------------------------------------------- 1 | package imguiext 2 | 3 | import ( 4 | "sdmm/internal/platform" 5 | 6 | "github.com/SpaiR/imgui-go" 7 | "github.com/go-gl/glfw/v3.3/glfw" 8 | ) 9 | 10 | func SetItemHoveredTooltip(text string) { 11 | if imgui.IsItemHovered() { 12 | imgui.SetTooltip(text) 13 | } 14 | } 15 | 16 | func InputIntClamp( 17 | label string, 18 | v *int32, 19 | min, max, step, stepFast int, 20 | ) bool { 21 | if imgui.InputIntV(label, v, step, stepFast, imgui.InputTextFlagsNone) { 22 | if int(*v) > max { 23 | *v = int32(max) 24 | } else if int(*v) < min { 25 | *v = int32(min) 26 | } 27 | return true 28 | } 29 | return false 30 | } 31 | 32 | func IsAltDown() bool { 33 | return imgui.IsKeyDown(int(glfw.KeyLeftAlt)) || imgui.IsKeyDown(int(glfw.KeyRightAlt)) 34 | } 35 | 36 | func IsShiftDown() bool { 37 | return imgui.IsKeyDown(int(glfw.KeyLeftShift)) || imgui.IsKeyDown(int(glfw.KeyRightShift)) 38 | } 39 | 40 | func IsCtrlDown() bool { 41 | return imgui.IsKeyDown(int(glfw.KeyLeftControl)) || imgui.IsKeyDown(int(glfw.KeyRightControl)) 42 | } 43 | 44 | func IsModDown() bool { 45 | return imgui.IsKeyDown(int(platform.KeyModLeft())) || imgui.IsKeyDown(int(platform.KeyModRight())) 46 | } 47 | -------------------------------------------------------------------------------- /internal/imguiext/style/colors.go: -------------------------------------------------------------------------------- 1 | package style 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/SpaiR/imgui-go" 7 | ) 8 | 9 | var ( 10 | ColorWhite = imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1} 11 | ColorZero = imgui.Vec4{} 12 | ColorBlack = imgui.Vec4{X: 0, Y: 0, Z: 0, W: 1} 13 | 14 | ColorGold = intHsl2col(51, 100, 50) 15 | ColorGoldLighter = intHsl2col(51, 100, 60) 16 | ColorGoldDarker = intHsl2col(51, 100, 40) 17 | 18 | ColorRed = intHsl2col(4, 60, 47) 19 | ColorRedLighter = intHsl2col(4, 60, 57) 20 | ColorRedDarker = intHsl2col(4, 60, 37) 21 | 22 | ColorFireCoral = intHsl2col(360, 50, 50) 23 | ColorFireCoralLighter = intHsl2col(360, 50, 60) 24 | ColorFireCoralDarker = intHsl2col(360, 50, 40) 25 | 26 | ColorGreen1 = intHsl2col(112, 89, 28) 27 | ColorGreen1Lighter = intHsl2col(112, 89, 38) 28 | ColorGreen1Darker = intHsl2col(112, 89, 18) 29 | 30 | ColorGreen2 = intHsl2col(109, 85, 41) 31 | ColorGreen2Lighter = intHsl2col(109, 85, 51) 32 | ColorGreen2Darker = intHsl2col(109, 85, 31) 33 | 34 | ColorGreen3 = intHsl2col(103, 100, 49) 35 | ColorGreen3Lighter = intHsl2col(103, 100, 59) 36 | ColorGreen3Darker = intHsl2col(103, 100, 39) 37 | 38 | ColorTransparent = imgui.Vec4{W: 0} 39 | 40 | ColorWhitePacked = imgui.Packed(color.RGBA{R: 255, G: 255, B: 255, A: 255}) 41 | ) 42 | 43 | func float2colV(r, g, b, a float32) imgui.Vec4 { 44 | return imgui.Vec4{X: r, Y: g, Z: b, W: a} 45 | } 46 | 47 | func intHsl2col(h, s, l int) imgui.Vec4 { 48 | return hsl2col(float32(h)/360, float32(s)/100, float32(l)/100) 49 | } 50 | 51 | func hsl2col(h, s, l float32) imgui.Vec4 { 52 | return hsl2colV(h, s, l, 1) 53 | } 54 | 55 | func hsl2colV(h, s, l, a float32) imgui.Vec4 { 56 | var q, p, r, g, b float32 57 | 58 | if s == 0 { 59 | r, g, b = l, l, l 60 | } else { 61 | if l < .5 { 62 | q = l * (1 + s) 63 | } else { 64 | q = l + s - l*s 65 | } 66 | p = 2*l - q 67 | r = hue2rgb(p, q, h+1.0/3) 68 | g = hue2rgb(p, q, h) 69 | b = hue2rgb(p, q, h-1.0/3) 70 | } 71 | 72 | return float2colV(r, g, b, a) 73 | } 74 | 75 | func hue2rgb(p, q, hue float32) float32 { 76 | h := hue 77 | if h < 0 { 78 | h += 1 79 | } 80 | if h > 1 { 81 | h -= 1 82 | } 83 | if 6*h < 1 { 84 | return p + ((q - p) * 6 * h) 85 | } 86 | if 2*h < 1 { 87 | return q 88 | } 89 | if 3*h < 2 { 90 | return p + ((q - p) * 6 * ((2.0 / 3.0) - h)) 91 | } 92 | return p 93 | } 94 | -------------------------------------------------------------------------------- /internal/imguiext/style/style.go: -------------------------------------------------------------------------------- 1 | package style 2 | 3 | import ( 4 | "github.com/SpaiR/imgui-go" 5 | ) 6 | 7 | type ButtonGreen struct { 8 | } 9 | 10 | func (ButtonGreen) NormalColor() imgui.Vec4 { 11 | return ColorGreen1 12 | } 13 | 14 | func (ButtonGreen) ActiveColor() imgui.Vec4 { 15 | return ColorGreen1Darker 16 | } 17 | 18 | func (ButtonGreen) HoverColor() imgui.Vec4 { 19 | return ColorGreen1Lighter 20 | } 21 | 22 | type ButtonDefault struct { 23 | } 24 | 25 | func (ButtonDefault) NormalColor() imgui.Vec4 { 26 | return imgui.CurrentStyle().Color(imgui.StyleColorButton) 27 | } 28 | 29 | func (ButtonDefault) ActiveColor() imgui.Vec4 { 30 | return imgui.CurrentStyle().Color(imgui.StyleColorButtonActive) 31 | } 32 | 33 | func (ButtonDefault) HoverColor() imgui.Vec4 { 34 | return imgui.CurrentStyle().Color(imgui.StyleColorButtonHovered) 35 | } 36 | 37 | type ButtonGold struct { 38 | } 39 | 40 | func (ButtonGold) NormalColor() imgui.Vec4 { 41 | return ColorGold 42 | } 43 | 44 | func (ButtonGold) ActiveColor() imgui.Vec4 { 45 | return ColorGoldDarker 46 | } 47 | 48 | func (ButtonGold) HoverColor() imgui.Vec4 { 49 | return ColorGoldLighter 50 | } 51 | 52 | type ButtonRed struct { 53 | } 54 | 55 | func (ButtonRed) NormalColor() imgui.Vec4 { 56 | return ColorRed 57 | } 58 | 59 | func (ButtonRed) ActiveColor() imgui.Vec4 { 60 | return ColorRedDarker 61 | } 62 | 63 | func (ButtonRed) HoverColor() imgui.Vec4 { 64 | return ColorRedLighter 65 | } 66 | 67 | type ButtonTransparent struct { 68 | } 69 | 70 | func (ButtonTransparent) NormalColor() imgui.Vec4 { 71 | return ColorZero 72 | } 73 | 74 | func (ButtonTransparent) ActiveColor() imgui.Vec4 { 75 | return ColorZero 76 | } 77 | 78 | func (ButtonTransparent) HoverColor() imgui.Vec4 { 79 | return ColorZero 80 | } 81 | 82 | type ButtonFrame struct { 83 | } 84 | 85 | func (ButtonFrame) NormalColor() imgui.Vec4 { 86 | return imgui.CurrentStyle().Color(imgui.StyleColorFrameBg) 87 | } 88 | 89 | func (ButtonFrame) ActiveColor() imgui.Vec4 { 90 | return imgui.CurrentStyle().Color(imgui.StyleColorFrameBgActive) 91 | } 92 | 93 | func (ButtonFrame) HoverColor() imgui.Vec4 { 94 | return imgui.CurrentStyle().Color(imgui.StyleColorFrameBgHovered) 95 | } 96 | 97 | type ButtonFireCoral struct { 98 | } 99 | 100 | func (ButtonFireCoral) NormalColor() imgui.Vec4 { 101 | return ColorFireCoral 102 | } 103 | 104 | func (ButtonFireCoral) ActiveColor() imgui.Vec4 { 105 | return ColorFireCoralDarker 106 | } 107 | 108 | func (ButtonFireCoral) HoverColor() imgui.Vec4 { 109 | return ColorFireCoralLighter 110 | } 111 | -------------------------------------------------------------------------------- /internal/imguiext/widget/align_text_to_frame_padding.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type alignTextToFramePaddingWidget struct { 6 | } 7 | 8 | func (alignTextToFramePaddingWidget) Build() { 9 | imgui.AlignTextToFramePadding() 10 | } 11 | 12 | func AlignTextToFramePadding() *alignTextToFramePaddingWidget { 13 | return &alignTextToFramePaddingWidget{} 14 | } 15 | -------------------------------------------------------------------------------- /internal/imguiext/widget/custom.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | type customWidget struct { 4 | builder func() 5 | } 6 | 7 | func (c *customWidget) Build() { 8 | c.builder() 9 | } 10 | 11 | func Custom(builder func()) *customWidget { 12 | return &customWidget{ 13 | builder: builder, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/imguiext/widget/disabled.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type disabledWidget struct { 6 | disabled bool 7 | layout Layout 8 | } 9 | 10 | func (s *disabledWidget) Build() { 11 | imgui.BeginDisabledV(s.disabled) 12 | s.layout.Build() 13 | imgui.EndDisabled() 14 | } 15 | 16 | func (s *disabledWidget) CalcSize() (size imgui.Vec2) { 17 | return s.layout.CalcSize() 18 | } 19 | 20 | func Disabled(disabled bool, layout ...widget) *disabledWidget { 21 | return &disabledWidget{ 22 | disabled, 23 | layout, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/imguiext/widget/dummy.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type dummyWidget struct { 6 | size imgui.Vec2 7 | } 8 | 9 | func (s *dummyWidget) Build() { 10 | imgui.Dummy(s.size) 11 | } 12 | 13 | func Dummy(size imgui.Vec2) *dummyWidget { 14 | return &dummyWidget{ 15 | size: size, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/imguiext/widget/font.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type fontWidget struct { 6 | font imgui.Font 7 | content Layout 8 | } 9 | 10 | func (w *fontWidget) Build() { 11 | imgui.PushFont(w.font) 12 | w.content.Build() 13 | imgui.PopFont() 14 | } 15 | 16 | func Font(font imgui.Font, content ...widget) *fontWidget { 17 | return &fontWidget{ 18 | font: font, 19 | content: content, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/imguiext/widget/group.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type Group Layout 6 | 7 | func (group Group) Build() { 8 | imgui.BeginGroup() 9 | for _, w := range group { 10 | w.Build() 11 | } 12 | imgui.EndGroup() 13 | } 14 | -------------------------------------------------------------------------------- /internal/imguiext/widget/image.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type imageWidget struct { 6 | texture imgui.TextureID 7 | size imgui.Vec2 8 | uv0, uv1 imgui.Vec2 9 | tintColor, borderColor imgui.Vec4 10 | } 11 | 12 | func (i *imageWidget) Uv(uv0, uv1 imgui.Vec2) *imageWidget { 13 | i.uv0, i.uv1 = uv0, uv1 14 | return i 15 | } 16 | 17 | func (i *imageWidget) TintColor(tintColor imgui.Vec4) *imageWidget { 18 | i.tintColor = tintColor 19 | return i 20 | } 21 | 22 | func (i *imageWidget) BorderColor(borderColor imgui.Vec4) *imageWidget { 23 | i.borderColor = borderColor 24 | return i 25 | } 26 | 27 | func (i *imageWidget) Build() { 28 | imgui.ImageV(i.texture, i.size, i.uv0, i.uv1, i.tintColor, i.borderColor) 29 | } 30 | 31 | func Image(texture imgui.TextureID, width, height float32) *imageWidget { 32 | return &imageWidget{ 33 | texture: texture, 34 | size: imgui.Vec2{X: width, Y: height}, 35 | uv0: imgui.Vec2{}, 36 | uv1: imgui.Vec2{X: 1, Y: 1}, 37 | tintColor: imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1}, 38 | borderColor: imgui.Vec4{}, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/imguiext/widget/indent.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type indentWidget struct { 6 | indent float32 7 | content Layout 8 | } 9 | 10 | func (w *indentWidget) Build() { 11 | imgui.IndentV(w.indent) 12 | w.content.Build() 13 | imgui.UnindentV(w.indent) 14 | } 15 | 16 | func Indent(indent float32, content ...widget) *indentWidget { 17 | return &indentWidget{ 18 | indent: indent, 19 | content: content, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/imguiext/widget/input_text.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "github.com/SpaiR/imgui-go" 5 | ) 6 | 7 | type inputTextWidget struct { 8 | inputTextFunc func() bool 9 | 10 | label string 11 | text *string 12 | width float32 13 | button *ButtonWidget 14 | flags imgui.InputTextFlags 15 | cb imgui.InputTextCallback 16 | onChange func() 17 | onDeactivatedAfterEdit func() 18 | } 19 | 20 | func (i *inputTextWidget) Width(width float32) *inputTextWidget { 21 | i.width = width 22 | return i 23 | } 24 | 25 | func (i *inputTextWidget) Button(button *ButtonWidget) *inputTextWidget { 26 | i.button = button 27 | return i 28 | } 29 | 30 | func (i *inputTextWidget) Flags(flags imgui.InputTextFlags) *inputTextWidget { 31 | i.flags = flags 32 | return i 33 | } 34 | 35 | func (i *inputTextWidget) Callback(cb imgui.InputTextCallback) *inputTextWidget { 36 | i.cb = cb 37 | return i 38 | } 39 | 40 | func (i *inputTextWidget) OnChange(onChange func()) *inputTextWidget { 41 | i.onChange = onChange 42 | return i 43 | } 44 | 45 | func (i *inputTextWidget) OnDeactivatedAfterEdit(onDeactivatedAfterEdit func()) *inputTextWidget { 46 | i.onDeactivatedAfterEdit = onDeactivatedAfterEdit 47 | return i 48 | } 49 | 50 | func (i *inputTextWidget) Build() { 51 | widgetWidth := i.width 52 | frameRounding := imgui.CurrentStyle().FrameRounding() 53 | 54 | if i.button != nil && widgetWidth != 0 { 55 | if widgetWidth == -1 { 56 | widgetWidth = -i.button.CalcSize().X + frameRounding 57 | } else { 58 | widgetWidth -= i.button.CalcSize().X + frameRounding 59 | } 60 | } 61 | 62 | if widgetWidth != 0 { 63 | imgui.SetNextItemWidth(widgetWidth) 64 | } 65 | 66 | if i.button != nil { 67 | // Shift input over the button to remove the gap between two widgets. 68 | imgui.PushStyleVarVec2(imgui.StyleVarItemSpacing, imgui.Vec2{X: -frameRounding, Y: imgui.CurrentStyle().ItemSpacing().Y}) 69 | } 70 | 71 | if i.inputTextFunc() && i.onChange != nil { 72 | i.onChange() 73 | } 74 | if i.onDeactivatedAfterEdit != nil && imgui.IsItemDeactivatedAfterEdit() { 75 | i.onDeactivatedAfterEdit() 76 | } 77 | 78 | if i.button != nil { 79 | imgui.SameLine() 80 | i.button.Build() 81 | imgui.PopStyleVar() 82 | } 83 | } 84 | 85 | func InputText(label string, text *string) *inputTextWidget { 86 | i := &inputTextWidget{ 87 | label: label, 88 | text: text, 89 | } 90 | i.inputTextFunc = func() bool { 91 | return imgui.InputTextV(i.label, i.text, i.flags, i.cb) 92 | } 93 | return i 94 | } 95 | -------------------------------------------------------------------------------- /internal/imguiext/widget/input_text_multiline.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type inputTextMultilineWidget struct { 6 | label string 7 | text *string 8 | width, height float32 9 | flags imgui.InputTextFlags 10 | cb imgui.InputTextCallback 11 | onChange func() 12 | } 13 | 14 | func (i *inputTextMultilineWidget) Size(width, height float32) *inputTextMultilineWidget { 15 | i.width = width 16 | i.height = height 17 | return i 18 | } 19 | 20 | func (i *inputTextMultilineWidget) Flags(flags imgui.InputTextFlags) *inputTextMultilineWidget { 21 | i.flags = flags 22 | return i 23 | } 24 | 25 | func (i *inputTextMultilineWidget) Callback(cb imgui.InputTextCallback) *inputTextMultilineWidget { 26 | i.cb = cb 27 | return i 28 | } 29 | 30 | func (i *inputTextMultilineWidget) OnChange(onChange func()) *inputTextMultilineWidget { 31 | i.onChange = onChange 32 | return i 33 | } 34 | 35 | func (i *inputTextMultilineWidget) Build() { 36 | if imgui.InputTextMultilineV(i.label, i.text, imgui.Vec2{X: i.width, Y: i.height}, i.flags, i.cb) && i.onChange != nil { 37 | i.onChange() 38 | } 39 | } 40 | 41 | func InputTextMultiline(label string, text *string) *inputTextMultilineWidget { 42 | return &inputTextMultilineWidget{ 43 | label: label, 44 | text: text, 45 | width: 0, 46 | height: 0, 47 | flags: imgui.InputTextFlagsNone, 48 | cb: nil, 49 | onChange: nil, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/imguiext/widget/input_text_with_hint.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "sdmm/internal/imguiext/icon" 5 | "sdmm/internal/imguiext/style" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | ) 9 | 10 | type inputTextWithHintWidget struct { 11 | inputTextWidget 12 | hint string 13 | } 14 | 15 | func (i *inputTextWithHintWidget) ButtonClear() *inputTextWithHintWidget { 16 | fClear := func() { 17 | *i.text = "" 18 | } 19 | i.Button(Button(icon.Clear+"##"+i.label, fClear). 20 | TextColor(imgui.CurrentStyle().Color(imgui.StyleColorTextDisabled)). 21 | Style(style.ButtonFrame{}), 22 | ) 23 | return i 24 | } 25 | 26 | func InputTextWithHint(label, hint string, text *string) *inputTextWithHintWidget { 27 | i := &inputTextWithHintWidget{hint: hint} 28 | i.label = label 29 | i.text = text 30 | i.inputTextFunc = func() bool { 31 | return imgui.InputTextWithHintV(i.label, i.hint, i.text, i.flags, i.cb) 32 | } 33 | return i 34 | } 35 | -------------------------------------------------------------------------------- /internal/imguiext/widget/layout.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type Layout []widget 6 | 7 | func (layout Layout) Build() { 8 | if align, ok := layout[0].(LayoutAlign); ok { 9 | layout.BuildV(align) 10 | } else { 11 | layout.BuildV(AlignLeft) 12 | } 13 | } 14 | 15 | func (layout Layout) CalcSize() (size imgui.Vec2) { 16 | _, hasAlign := layout[0].(LayoutAlign) 17 | 18 | for idx, w := range layout { 19 | if r, ok := w.(region); ok { 20 | size = size.Plus(r.CalcSize()) 21 | if hasAlign && idx > 1 || !hasAlign && idx > 0 { 22 | size = size.Plus(imgui.CurrentStyle().ItemSpacing()) 23 | } 24 | } 25 | } 26 | return size 27 | } 28 | 29 | type LayoutAlign int 30 | 31 | func (LayoutAlign) Build() { 32 | // mock widget behaviour 33 | } 34 | 35 | type region interface { 36 | CalcSize() imgui.Vec2 37 | } 38 | 39 | const ( 40 | AlignLeft LayoutAlign = iota 41 | AlignCenter 42 | AlignRight 43 | ) 44 | 45 | func (layout Layout) BuildV(align LayoutAlign) { 46 | var indent float32 47 | switch align { 48 | case AlignLeft: 49 | indent = 0 50 | case AlignRight: 51 | indent = imgui.WindowWidth() - imgui.CurrentStyle().WindowPadding().Times(2).X - layout.CalcSize().X 52 | } 53 | 54 | if indent != 0 { 55 | imgui.IndentV(indent) 56 | } 57 | for _, w := range layout { 58 | w.Build() 59 | } 60 | if indent != 0 { 61 | imgui.UnindentV(indent) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/imguiext/widget/line.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type LineWidget struct { 6 | content Layout 7 | } 8 | 9 | func (w *LineWidget) CalcSize() imgui.Vec2 { 10 | return w.content.CalcSize() 11 | } 12 | 13 | func (w *LineWidget) Build() { 14 | cnt := Layout{} 15 | for idx, l := range w.content { 16 | if idx != len(w.content)-1 { 17 | cnt = append(cnt, l, SameLine()) 18 | } else { 19 | cnt = append(cnt, l) 20 | } 21 | } 22 | cnt.Build() 23 | } 24 | 25 | func Line(content ...widget) *LineWidget { 26 | return &LineWidget{ 27 | content: content, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/imguiext/widget/main_menu_bar.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type mainMenuBarWidget struct { 6 | layout Layout 7 | } 8 | 9 | func (m *mainMenuBarWidget) Build() { 10 | if imgui.BeginMainMenuBar() { 11 | if m.layout != nil { 12 | m.layout.Build() 13 | } 14 | imgui.EndMainMenuBar() 15 | } 16 | } 17 | 18 | func MainMenuBar(layout Layout) *mainMenuBarWidget { 19 | return &mainMenuBarWidget{ 20 | layout: layout, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/imguiext/widget/menu.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "sdmm/internal/imguiext/style" 5 | 6 | "github.com/SpaiR/imgui-go" 7 | ) 8 | 9 | // Placeholder to check if there is an empty icon for the menu. 10 | const mHolderEmptyIcon = "!/emptyIcon/!" 11 | 12 | type menuWidget struct { 13 | label string 14 | enabled bool 15 | icon string 16 | layout Layout 17 | } 18 | 19 | func (m *menuWidget) Enabled(enabled bool) *menuWidget { 20 | m.enabled = enabled 21 | return m 22 | } 23 | 24 | func (m *menuWidget) Icon(icon string) *menuWidget { 25 | m.icon = icon 26 | return m 27 | } 28 | 29 | func (m *menuWidget) IconEmpty() *menuWidget { 30 | m.icon = mHolderEmptyIcon 31 | return m 32 | } 33 | 34 | func (m *menuWidget) Build() { 35 | label := m.label 36 | 37 | var iconPos imgui.Vec2 38 | var iconCol imgui.PackedColor 39 | if len(m.icon) != 0 { 40 | // Add padding to the label text. This padding will be filled with the icon. 41 | label = " " + label 42 | 43 | // Draw a dummy. It's needed to correctly process an icon padding. 44 | cursorPos := imgui.CursorPos() 45 | imgui.Dummy(imgui.Vec2{}) 46 | imgui.SameLine() 47 | imgui.SetCursorPos(cursorPos) 48 | iconPos = imgui.ItemRectMin() 49 | 50 | if m.enabled { 51 | iconCol = style.ColorWhitePacked 52 | } else { 53 | iconCol = imgui.PackedColorFromVec4(imgui.CurrentStyle().Color(imgui.StyleColorTextDisabled)) 54 | } 55 | } 56 | 57 | if imgui.BeginMenuV(label, m.enabled) { 58 | if m.layout != nil { 59 | m.layout.Build() 60 | } 61 | imgui.EndMenu() 62 | } 63 | 64 | if len(m.icon) != 0 && m.icon != miHolderEmptyIcon { 65 | imgui.WindowDrawList().AddText(iconPos, iconCol, m.icon) 66 | } 67 | } 68 | 69 | func Menu(label string, layout Layout) *menuWidget { 70 | return &menuWidget{ 71 | label: label, 72 | enabled: true, 73 | layout: layout, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/imguiext/widget/menu_item.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "sdmm/internal/app/ui/shortcut" 5 | "sdmm/internal/imguiext/style" 6 | 7 | "github.com/SpaiR/imgui-go" 8 | ) 9 | 10 | // Placeholder to check if there is an empty icon for the menuItem. 11 | const miHolderEmptyIcon = "!/emptyIcon/!" 12 | 13 | type menuItemWidget struct { 14 | label string 15 | shortcut string 16 | selected bool 17 | enabled bool 18 | icon string 19 | onClick func() 20 | } 21 | 22 | func (m *menuItemWidget) Shortcut(keys ...string) *menuItemWidget { 23 | m.shortcut = shortcut.Combine(keys...) 24 | return m 25 | } 26 | 27 | func (m *menuItemWidget) Selected(selected bool) *menuItemWidget { 28 | m.selected = selected 29 | return m 30 | } 31 | 32 | func (m *menuItemWidget) Enabled(enabled bool) *menuItemWidget { 33 | m.enabled = enabled 34 | return m 35 | } 36 | 37 | func (m *menuItemWidget) Icon(icon string) *menuItemWidget { 38 | m.icon = icon 39 | return m 40 | } 41 | 42 | func (m *menuItemWidget) IconEmpty() *menuItemWidget { 43 | m.icon = miHolderEmptyIcon 44 | return m 45 | } 46 | 47 | func (m *menuItemWidget) Build() { 48 | label := m.label 49 | 50 | var iconPos imgui.Vec2 51 | if len(m.icon) != 0 { 52 | // Add padding to the label text. This padding will be filled with the icon. 53 | label = " " + label 54 | 55 | // Draw a dummy. It's needed to correctly process an icon padding. 56 | cursorPos := imgui.CursorPos() 57 | imgui.Dummy(imgui.Vec2{}) 58 | imgui.SameLine() 59 | imgui.SetCursorPos(cursorPos) 60 | iconPos = imgui.ItemRectMin() 61 | } 62 | 63 | if imgui.MenuItemV(label, m.shortcut, m.selected, m.enabled) && m.onClick != nil { 64 | m.onClick() 65 | } 66 | 67 | if len(m.icon) != 0 && m.icon != miHolderEmptyIcon { 68 | var iconCol imgui.PackedColor 69 | if m.enabled { 70 | iconCol = style.ColorWhitePacked 71 | } else { 72 | iconCol = imgui.PackedColorFromVec4(imgui.CurrentStyle().Color(imgui.StyleColorTextDisabled)) 73 | } 74 | imgui.WindowDrawList().AddText(iconPos, iconCol, m.icon) 75 | } 76 | } 77 | 78 | func MenuItem(label string, onClick func()) *menuItemWidget { 79 | return &menuItemWidget{ 80 | label: label, 81 | enabled: true, 82 | onClick: onClick, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/imguiext/widget/new_line.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type newLineWidget struct{} 6 | 7 | func (s *newLineWidget) Build() { 8 | imgui.NewLine() 9 | } 10 | 11 | func NewLine() *newLineWidget { 12 | return &newLineWidget{} 13 | } 14 | -------------------------------------------------------------------------------- /internal/imguiext/widget/same_line.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type sameLineWidget struct{} 6 | 7 | func (s *sameLineWidget) Build() { 8 | imgui.SameLine() 9 | } 10 | 11 | func SameLine() *sameLineWidget { 12 | return &sameLineWidget{} 13 | } 14 | -------------------------------------------------------------------------------- /internal/imguiext/widget/selectable.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type SelectableWidget struct { 6 | label string 7 | selected bool 8 | flags imgui.SelectableFlags 9 | size imgui.Vec2 10 | mouseCursor imgui.MouseCursorID 11 | onClick func() 12 | } 13 | 14 | func (w *SelectableWidget) Selected(selected bool) *SelectableWidget { 15 | w.selected = selected 16 | return w 17 | } 18 | 19 | func (w *SelectableWidget) Flags(flags imgui.SelectableFlags) *SelectableWidget { 20 | w.flags = flags 21 | return w 22 | } 23 | 24 | func (w *SelectableWidget) Size(size imgui.Vec2) *SelectableWidget { 25 | w.size = size 26 | return w 27 | } 28 | 29 | func (w *SelectableWidget) Mouse(mouse imgui.MouseCursorID) *SelectableWidget { 30 | w.mouseCursor = mouse 31 | return w 32 | } 33 | 34 | func (w *SelectableWidget) OnClick(onClick func()) *SelectableWidget { 35 | w.onClick = onClick 36 | return w 37 | } 38 | 39 | func (w *SelectableWidget) Build() { 40 | if imgui.SelectableV(w.label, w.selected, w.flags, w.size) { 41 | if w.onClick != nil { 42 | w.onClick() 43 | } 44 | } 45 | if imgui.IsItemHovered() { 46 | imgui.SetMouseCursor(w.mouseCursor) 47 | } 48 | } 49 | 50 | func Selectable(label string) *SelectableWidget { 51 | return &SelectableWidget{ 52 | label: label, 53 | mouseCursor: imgui.MouseCursorArrow, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/imguiext/widget/separator.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type separatorWidget struct{} 6 | 7 | func (separatorWidget) Build() { 8 | imgui.Separator() 9 | } 10 | 11 | func Separator() *separatorWidget { 12 | return &separatorWidget{} 13 | } 14 | -------------------------------------------------------------------------------- /internal/imguiext/widget/text.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type textWidget struct { 6 | text string 7 | } 8 | 9 | func (t *textWidget) Build() { 10 | imgui.Text(t.text) 11 | } 12 | 13 | func (t *textWidget) CalcSize() imgui.Vec2 { 14 | return imgui.CalcTextSize(t.text, true, -1) 15 | } 16 | 17 | func Text(text string) *textWidget { 18 | return &textWidget{ 19 | text: text, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/imguiext/widget/text_colored.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type textColoredWidget struct { 6 | text string 7 | color imgui.Vec4 8 | } 9 | 10 | func (t *textColoredWidget) Build() { 11 | imgui.TextColored(t.color, t.text) 12 | } 13 | 14 | func (t *textColoredWidget) CalcSize() imgui.Vec2 { 15 | return imgui.CalcTextSize(t.text, true, -1) 16 | } 17 | 18 | func TextColored(text string, color imgui.Vec4) *textColoredWidget { 19 | return &textColoredWidget{ 20 | text: text, 21 | color: color, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/imguiext/widget/text_disabled.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type textDisabledWidget struct { 6 | text string 7 | } 8 | 9 | func (t *textDisabledWidget) Build() { 10 | imgui.TextDisabled(t.text) 11 | } 12 | 13 | func (t *textDisabledWidget) CalcSize() imgui.Vec2 { 14 | return imgui.CalcTextSize(t.text, true, -1) 15 | } 16 | 17 | func TextDisabled(text string) *textDisabledWidget { 18 | return &textDisabledWidget{ 19 | text: text, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/imguiext/widget/text_frame.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type textFrameWidget struct { 6 | buttonWidget *ButtonWidget 7 | 8 | width float32 9 | } 10 | 11 | func (w *textFrameWidget) Width(width float32) *textFrameWidget { 12 | w.width = width 13 | return w 14 | } 15 | 16 | func (w *textFrameWidget) Build() { 17 | if w.width != 0 { 18 | w.buttonWidget.size = imgui.Vec2{X: w.width} 19 | } 20 | col := imgui.CurrentStyle().Color(imgui.StyleColorButton) 21 | col.W = .5 22 | w.buttonWidget. 23 | NormalColor(col). 24 | ActiveColor(col). 25 | HoverColor(col). 26 | Build() 27 | } 28 | 29 | func (w *textFrameWidget) CalcSize() imgui.Vec2 { 30 | return w.buttonWidget.CalcSize() 31 | } 32 | 33 | func TextFrame(text string) *textFrameWidget { 34 | return &textFrameWidget{ 35 | buttonWidget: Button(text, nil), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/imguiext/widget/text_wrapped.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type textWrappedWidget struct { 6 | text string 7 | } 8 | 9 | func (t *textWrappedWidget) Build() { 10 | imgui.TextWrapped(t.text) 11 | } 12 | 13 | func TextWrapped(text string) *textWrappedWidget { 14 | return &textWrappedWidget{ 15 | text: text, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/imguiext/widget/tooltip.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type tooltipWidget struct { 6 | content Layout 7 | onHover bool 8 | } 9 | 10 | func (w *tooltipWidget) OnHover(onHover bool) *tooltipWidget { 11 | w.onHover = onHover 12 | return w 13 | } 14 | 15 | func (w *tooltipWidget) Build() { 16 | if w.onHover && imgui.IsItemHovered() || !w.onHover { 17 | imgui.BeginTooltip() 18 | w.content.Build() 19 | imgui.EndTooltip() 20 | } 21 | } 22 | 23 | func Tooltip(content ...widget) *tooltipWidget { 24 | return &tooltipWidget{ 25 | content: content, 26 | onHover: true, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/imguiext/widget/widget.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | type widget interface { 4 | Build() 5 | } 6 | -------------------------------------------------------------------------------- /internal/imguiext/widget/window.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import "github.com/SpaiR/imgui-go" 4 | 5 | type windowWidget struct { 6 | id string 7 | open *bool 8 | flags imgui.WindowFlags 9 | layout Layout 10 | push func() 11 | pop func() 12 | } 13 | 14 | func (w *windowWidget) Open(open *bool) *windowWidget { 15 | w.open = open 16 | return w 17 | } 18 | 19 | func (w *windowWidget) Flags(flags imgui.WindowFlags) *windowWidget { 20 | w.flags = flags 21 | return w 22 | } 23 | 24 | func (w *windowWidget) Push(push func()) *windowWidget { 25 | w.push = push 26 | return w 27 | } 28 | 29 | func (w *windowWidget) Pop(pop func()) *windowWidget { 30 | w.pop = pop 31 | return w 32 | } 33 | 34 | func (w *windowWidget) Build() { 35 | if w.push != nil { 36 | w.push() 37 | } 38 | if imgui.BeginV(w.id, w.open, w.flags) { 39 | if w.pop != nil { 40 | w.pop() 41 | } 42 | w.layout.Build() 43 | } else if w.pop != nil { 44 | w.pop() 45 | } 46 | imgui.End() 47 | } 48 | 49 | func Window(id string, layout Layout) *windowWidget { 50 | return &windowWidget{ 51 | id: id, 52 | open: nil, 53 | flags: 0, 54 | layout: layout, 55 | push: nil, 56 | pop: nil, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/platform/key.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/go-gl/glfw/v3.3/glfw" 7 | ) 8 | 9 | var isDarwin = runtime.GOOS == "darwin" 10 | 11 | func KeyModName() string { 12 | if isDarwin { 13 | return "Cmd" 14 | } 15 | return "Ctrl" 16 | } 17 | 18 | func KeyModLeft() glfw.Key { 19 | if isDarwin { 20 | return glfw.KeyLeftSuper 21 | } 22 | return glfw.KeyLeftControl 23 | } 24 | 25 | func KeyModRight() glfw.Key { 26 | if isDarwin { 27 | return glfw.KeyRightSuper 28 | } 29 | return glfw.KeyRightControl 30 | } 31 | -------------------------------------------------------------------------------- /internal/platform/shader.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/go-gl/gl/v3.3-core/gl" 8 | ) 9 | 10 | func NewShaderProgram(vertexShaderSource, fragmentShaderSource string) (uint32, error) { 11 | vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER) 12 | if err != nil { 13 | return 0, err 14 | } 15 | 16 | fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER) 17 | if err != nil { 18 | return 0, err 19 | } 20 | 21 | program := gl.CreateProgram() 22 | 23 | gl.AttachShader(program, vertexShader) 24 | gl.AttachShader(program, fragmentShader) 25 | gl.LinkProgram(program) 26 | 27 | var status int32 28 | gl.GetProgramiv(program, gl.LINK_STATUS, &status) 29 | if status == gl.FALSE { 30 | var logLength int32 31 | gl.GetProgramiv(program, gl.INFO_LOG_LENGTH, &logLength) 32 | 33 | log := strings.Repeat("\x00", int(logLength+1)) 34 | gl.GetProgramInfoLog(program, logLength, nil, gl.Str(log)) 35 | 36 | return 0, fmt.Errorf("failed to link program: %v", log) 37 | } 38 | 39 | gl.DeleteShader(vertexShader) 40 | gl.DeleteShader(fragmentShader) 41 | 42 | return program, nil 43 | } 44 | 45 | func compileShader(source string, shaderType uint32) (uint32, error) { 46 | shader := gl.CreateShader(shaderType) 47 | 48 | src, free := gl.Strs(source) 49 | gl.ShaderSource(shader, 1, src, nil) 50 | free() 51 | gl.CompileShader(shader) 52 | 53 | var status int32 54 | gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) 55 | if status == gl.FALSE { 56 | var logLength int32 57 | gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength) 58 | 59 | log := strings.Repeat("\x00", int(logLength+1)) 60 | gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log)) 61 | 62 | return 0, fmt.Errorf("failed to compile %v: %v", source, log) 63 | } 64 | 65 | return shader, nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/platform/texture.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/go-gl/gl/v3.3-core/gl" 7 | ) 8 | 9 | func CreateTexture(img *image.NRGBA) uint32 { 10 | var lastTexture int32 11 | var handle uint32 12 | 13 | gl.GetIntegerv(gl.TEXTURE_BINDING_2D, &lastTexture) 14 | gl.GenTextures(1, &handle) 15 | gl.BindTexture(gl.TEXTURE_2D, handle) 16 | defer gl.BindTexture(gl.TEXTURE_2D, uint32(lastTexture)) 17 | 18 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 19 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 20 | 21 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR) 22 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 23 | 24 | gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(img.Bounds().Dx()), int32(img.Bounds().Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix)) 25 | gl.GenerateMipmap(gl.TEXTURE_2D) 26 | 27 | return handle 28 | } 29 | -------------------------------------------------------------------------------- /internal/req/req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | func Get(url string) (body []byte, err error) { 10 | req, err := http.NewRequest(http.MethodGet, url, nil) 11 | if err != nil { 12 | return nil, fmt.Errorf("fail to create a request to [%s]: %w", url, err) 13 | } 14 | 15 | client := http.Client{} 16 | resp, err := client.Do(req) 17 | if err != nil { 18 | return nil, fmt.Errorf("fail to get a response from [%s]: %w", url, err) 19 | } 20 | 21 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 22 | return nil, fmt.Errorf("fail to get successful response: code [%d]", resp.StatusCode) 23 | } 24 | 25 | if body, err = io.ReadAll(resp.Body); err != nil { 26 | return nil, fmt.Errorf("fail to read remote data: %w", err) 27 | } 28 | 29 | if err := resp.Body.Close(); err != nil { 30 | return nil, fmt.Errorf("fail to close response: %w", err) 31 | } 32 | 33 | return body, nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/rsc/font.go: -------------------------------------------------------------------------------- 1 | package rsc 2 | 3 | import _ "embed" 4 | 5 | var ( 6 | //go:embed font/Inter-Medium.ttf 7 | fontTTF []byte 8 | //go:embed font/icons/icomoon.ttf 9 | fontIconsTTF []byte 10 | ) 11 | 12 | func FontTTF() []byte { 13 | return fontTTF 14 | } 15 | 16 | func FontIconsTTF() []byte { 17 | return fontIconsTTF 18 | } 19 | -------------------------------------------------------------------------------- /internal/rsc/font/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/internal/rsc/font/Inter-Medium.ttf -------------------------------------------------------------------------------- /internal/rsc/font/icons/README.txt: -------------------------------------------------------------------------------- 1 | Icons for StrongDMM generated with https://icomoon.io/ 2 | File "selection.json" contains a "template" for the project used to generate icons. 3 | This file can be imported to the icomoon and modified appropriately. 4 | All new icons should be added using that application. 5 | -------------------------------------------------------------------------------- /internal/rsc/font/icons/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/internal/rsc/font/icons/icomoon.ttf -------------------------------------------------------------------------------- /internal/rsc/font/icons/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/internal/rsc/font/icons/icomoon.woff -------------------------------------------------------------------------------- /internal/rsc/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/internal/rsc/icon.ico -------------------------------------------------------------------------------- /internal/rsc/png.go: -------------------------------------------------------------------------------- 1 | package rsc 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "image" 7 | "image/draw" 8 | _ "image/png" 9 | 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | var ( 14 | //go:embed png/editor_texture_atlas.png 15 | editorSpriteAtlas []byte 16 | //go:embed png/editor_icon.png 17 | editorIcon []byte 18 | ) 19 | 20 | // EditorTextureAtlas returns a sprite atlas with all textures used by the editor. 21 | func EditorTextureAtlas() TextureAtlas { 22 | return TextureAtlas{ 23 | Width: 64, 24 | Height: 32, 25 | data: editorSpriteAtlas, 26 | } 27 | } 28 | 29 | func EditorIcon() TextureAtlas { 30 | return TextureAtlas{ 31 | Width: 1000, 32 | Height: 1000, 33 | data: editorIcon, 34 | } 35 | } 36 | 37 | type TextureAtlas struct { 38 | Width int 39 | Height int 40 | data []byte 41 | } 42 | 43 | func (a TextureAtlas) RGBA() *image.NRGBA { 44 | res, _, err := image.Decode(bytes.NewReader(a.data)) 45 | if err != nil { 46 | log.Panic().Msg("unable to decode texture atlas!") 47 | } 48 | img := image.NewNRGBA(image.Rect(0, 0, a.Width, a.Height)) 49 | draw.Draw(img, img.Bounds(), res, image.Point{}, draw.Src) 50 | return img 51 | } 52 | -------------------------------------------------------------------------------- /internal/rsc/png/editor_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/internal/rsc/png/editor_icon.png -------------------------------------------------------------------------------- /internal/rsc/png/editor_texture_atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpaiR/StrongDMM/3f32a2e32d76257547725b247682edb5c8f538d1/internal/rsc/png/editor_texture_atlas.png -------------------------------------------------------------------------------- /internal/rsc/rsc.go: -------------------------------------------------------------------------------- 1 | package rsc 2 | 3 | import ( 4 | _ "embed" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | //go:embed txt/about.txt 10 | aboutTxt string 11 | //go:embed txt/support.txt 12 | SupportTxt string 13 | //go:embed txt/changelog-header.txt 14 | ChangelogHeaderTxt string 15 | ChangelogMd string 16 | ) 17 | 18 | func AboutTxt(version string) string { 19 | return strings.Replace(aboutTxt, "%VERSION%", version, 1) 20 | } 21 | -------------------------------------------------------------------------------- /internal/rsc/txt/about.txt: -------------------------------------------------------------------------------- 1 | StrongDMM %VERSION% 2 | Copyright (C) 2019-2024, SpaiR 3 | 4 | This program comes with ABSOLUTELY NO WARRANTY. This is free software, 5 | and you are welcome to redistribute it under the conditions of the GNU 6 | General Public License version 3. 7 | -------------------------------------------------------------------------------- /internal/rsc/txt/changelog-header.txt: -------------------------------------------------------------------------------- 1 | Found an issue? Please report it to the bugtracker. Any sort of help is appreciated! If you have ideas to improve the editor, share them as well. 2 | If you like StrongDMM, feel free to Star the project on GitHub! 3 | -------------------------------------------------------------------------------- /internal/rsc/txt/support.txt: -------------------------------------------------------------------------------- 1 | StrongDMM developed without any monetization in mind. The main motivation is an enthusiasm to do a cool stuff. 2 | Your support can show your gratefulness and will motivate the project further development. 3 | -------------------------------------------------------------------------------- /internal/util/bounds.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "fmt" 4 | 5 | // Bounds stores a 2D area bounds in float32 values. 6 | type Bounds struct { 7 | X1, Y1 float32 8 | X2, Y2 float32 9 | } 10 | 11 | func (b Bounds) Plus(x, y float32) (result Bounds) { 12 | result.X1 = b.X1 + x 13 | result.Y1 = b.Y1 + y 14 | result.X2 = b.X2 + x 15 | result.Y2 = b.Y2 + y 16 | return result 17 | } 18 | 19 | // Contains returns true if the current Bounds contains received point. 20 | func (b Bounds) Contains(x, y float32) bool { 21 | return b.ContainsV(Bounds{x, y, x, y}) 22 | } 23 | 24 | // ContainsV returns true if the current Bounds contains received area. 25 | func (b Bounds) ContainsV(bounds Bounds) bool { 26 | return b.X2 >= bounds.X1 && b.Y2 >= bounds.Y1 && b.X1 <= bounds.X2 && b.Y1 <= bounds.Y2 27 | } 28 | 29 | func (b Bounds) String() string { 30 | return fmt.Sprintf("X1:%.0f, Y1:%.0f, X2:%.0f, Y2:%.0f", b.X1, b.Y1, b.X2, b.Y2) 31 | } 32 | 33 | func (b Bounds) IsEmpty() bool { 34 | return b.X1 == 0 && b.Y1 == 0 && b.X2 == 0 && b.Y2 == 0 35 | } 36 | -------------------------------------------------------------------------------- /internal/util/color.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/SpaiR/imgui-go" 5 | "github.com/mazznoer/csscolorparser" 6 | ) 7 | 8 | type Color struct { 9 | r, g, b, a float32 10 | } 11 | 12 | func MakeColor(r float32, g float32, b float32, a float32) Color { 13 | return Color{r: r, g: g, b: b, a: a} 14 | } 15 | 16 | func MakeColorFromVec4(col imgui.Vec4) Color { 17 | return MakeColor(col.X, col.Y, col.Z, col.W) 18 | } 19 | 20 | func (c Color) RGBA() (float32, float32, float32, float32) { 21 | return c.r, c.g, c.b, c.a 22 | } 23 | 24 | func (c Color) R() float32 { 25 | return c.r 26 | } 27 | 28 | func (c Color) G() float32 { 29 | return c.g 30 | } 31 | 32 | func (c Color) B() float32 { 33 | return c.b 34 | } 35 | 36 | func (c Color) A() float32 { 37 | return c.a 38 | } 39 | 40 | var ( 41 | parsedColorsCache map[string]csscolorparser.Color 42 | ) 43 | 44 | func init() { 45 | parsedColorsCache = make(map[string]csscolorparser.Color) 46 | } 47 | 48 | func ParseColor(color string) Color { 49 | var c csscolorparser.Color 50 | if col, ok := parsedColorsCache[color]; ok { 51 | c = col 52 | } else { 53 | if col, err := csscolorparser.Parse(color); err == nil { 54 | c = col 55 | } else { 56 | c = csscolorparser.Color{R: 1, G: 1, B: 1, A: 1} 57 | } 58 | parsedColorsCache[color] = c 59 | } 60 | return Color{ 61 | r: float32(c.R), 62 | g: float32(c.G), 63 | b: float32(c.B), 64 | a: float32(c.A), 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/util/point.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "fmt" 4 | 5 | // Point is a generic class to store a 3D point in space. 6 | type Point struct { 7 | X, Y, Z int 8 | } 9 | 10 | func (p Point) Plus(point Point) (result Point) { 11 | result.X = p.X + point.X 12 | result.Y = p.Y + point.Y 13 | result.Z = p.Z + point.Z 14 | return result 15 | } 16 | 17 | func (p Point) Minus(point Point) (result Point) { 18 | result.X = p.X - point.X 19 | result.Y = p.Y - point.Y 20 | result.Z = p.Z - point.Z 21 | return result 22 | } 23 | 24 | func (p Point) Equals(x, y, z int) bool { 25 | return p.X == x && p.Y == y && p.Z == z 26 | } 27 | 28 | func (p Point) String() string { 29 | return fmt.Sprintf("X:%d, Y:%d, Z:%d", p.X, p.Y, p.Z) 30 | } 31 | 32 | func (p Point) Copy() Point { 33 | return Point{ 34 | X: p.X, 35 | Y: p.Y, 36 | Z: p.Z, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/util/slice/slice.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | // StrContains returns true if the slice contains the provided string. 4 | func StrContains(slice []string, str string) bool { 5 | return StrIndexOf(slice, str) != -1 6 | } 7 | 8 | // StrPushUnique pushes string at the beginning of the slice. 9 | // If slice contains the string to push, the old one will be removed. 10 | func StrPushUnique(slice []string, str string) []string { 11 | if idx := StrIndexOf(slice, str); idx != -1 { 12 | return append([]string{str}, StrRemoveIdx(slice, idx)...) 13 | } else { 14 | return append([]string{str}, slice...) 15 | } 16 | } 17 | 18 | // StrIndexOf return the index of the provided string in the slice or -1. 19 | func StrIndexOf(slice []string, str string) int { 20 | for idx, s := range slice { 21 | if s == str { 22 | return idx 23 | } 24 | } 25 | return -1 26 | } 27 | 28 | // StrRemoveIdx removes an element from the slice by the index with order preserving. 29 | func StrRemoveIdx(slice []string, idx int) []string { 30 | if idx >= 0 && idx < len(slice) { 31 | return append(slice[:idx], slice[idx+1:]...) 32 | } 33 | return slice 34 | } 35 | 36 | // StrRemove removes an element from the slice. 37 | func StrRemove(slice []string, str string) []string { 38 | if idx := StrIndexOf(slice, str); idx != -1 { 39 | return StrRemoveIdx(slice, StrIndexOf(slice, str)) 40 | } 41 | return slice 42 | } 43 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/sqweek/dialog" 8 | ) 9 | 10 | // TimeFormat is const with format which need to be used everywhere we need to format time.Now() 11 | // Basically, just a time.DateTime with replaced ":" to ".". 12 | const TimeFormat = "2006-01-02 15.04.05" 13 | 14 | // Djb2 is hashing method implemented by spec: http://www.cse.yorku.ca/~oz/hash.html 15 | func Djb2(str string) uint64 { 16 | var hash uint64 = 5381 17 | for _, c := range str { 18 | hash = ((hash << 5) + hash) + uint64(c) 19 | } 20 | return hash 21 | } 22 | 23 | // ShowErrorDialog shows system error dialog to the user. 24 | // Accepts dialog message to show. 25 | func ShowErrorDialog(msg string) { 26 | ShowErrorDialogV("", msg) 27 | } 28 | 29 | // ShowErrorDialogV shows system error dialog to the user. 30 | // Accepts dialog title and message to show. 31 | func ShowErrorDialogV(title, msg string) { 32 | b := dialog.MsgBuilder{Msg: msg} 33 | b.Title(title) 34 | b.Error() 35 | } 36 | 37 | // PixelsToRGBA creates an RGBA image from provided raw pixels. 38 | func PixelsToRGBA(pixels []byte, w, h int) *image.RGBA { 39 | img := image.NewRGBA(image.Rect(0, 0, w, h)) 40 | 41 | for x := 0; x < w; x++ { 42 | for y := 0; y < h; y++ { 43 | pos := 4 * ((h-1-y)*w + x) 44 | r := pixels[pos] & 0xff 45 | g := pixels[pos+1] & 0xff 46 | b := pixels[pos+2] & 0xff 47 | a := pixels[pos+3] & 0xff 48 | if a != 0 { 49 | img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a}) 50 | } 51 | } 52 | } 53 | 54 | return img 55 | } 56 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "sdmm/internal/app" 7 | ) 8 | 9 | func main() { 10 | app.Start() 11 | os.Exit(0) 12 | } 13 | -------------------------------------------------------------------------------- /third_party/sdmmparser/lib/sdmmparser.h: -------------------------------------------------------------------------------- 1 | extern const char* SdmmParseEnvironment(const char* nativePath); 2 | extern const char* SdmmParseIconMetadata(const char* nativePath); 3 | extern void SdmmFreeStr(char* nativeStr); 4 | -------------------------------------------------------------------------------- /third_party/sdmmparser/src/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | -------------------------------------------------------------------------------- /third_party/sdmmparser/src/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sdmmparser" 3 | version = "2.0.0" 4 | rust-version = "1.82.0" 5 | edition = "2021" 6 | 7 | [lib] 8 | name = "sdmmparser" 9 | path = "lib.rs" 10 | crate-type = ["staticlib"] 11 | 12 | [dependencies] 13 | serde = "1.0.215" 14 | serde_derive = "1.0.215" 15 | serde_json = "1.0.133" 16 | png = "0.17.14" 17 | 18 | [dependencies.dreammaker] 19 | git = "https://github.com/SpaiR/SpacemanDMM" 20 | rev = "d19602a6f081e6877bb5de291b160000fdbd2aaa" 21 | package = "dreammaker" 22 | 23 | [profile.release] 24 | lto = true 25 | -------------------------------------------------------------------------------- /third_party/sdmmparser/src/icon.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, panic}; 2 | 3 | use dm::dmi::*; 4 | 5 | #[derive(Serialize)] 6 | struct IconMetadata { 7 | width: u32, 8 | height: u32, 9 | states: Vec, 10 | } 11 | 12 | #[derive(Serialize)] 13 | struct IconState { 14 | name: String, 15 | dirs: u32, 16 | frames: u32, 17 | } 18 | 19 | pub fn parse_icon_metadata(path: String) -> String { 20 | match panic::catch_unwind(|| match parse(&path) { 21 | Some(json) => json, 22 | None => format!("error: unable to parse icon metadata {}", path), 23 | }) { 24 | Ok(result) => result, 25 | Err(e) => { 26 | if let Some(e) = e.downcast_ref::() { 27 | format!("error: {}", e) 28 | } else { 29 | String::from("error: unknown") 30 | } 31 | } 32 | } 33 | } 34 | 35 | fn parse(path: &str) -> Option { 36 | fs::File::open(path).map_or(None, |f| { 37 | png::Decoder::new(f).read_info().map_or(None, |reader| { 38 | for text_chunk in &reader.info().compressed_latin1_text { 39 | if text_chunk.keyword.eq("Description") { 40 | return text_chunk.get_text().map_or(None, |info| { 41 | Metadata::meta_from_str(info.as_str()) 42 | .map_or(None, |metadata| Some(meta2json(metadata))) 43 | }); 44 | } 45 | } 46 | None 47 | }) 48 | }) 49 | } 50 | 51 | fn meta2json(metadata: Metadata) -> String { 52 | let mut states: Vec = Vec::new(); 53 | 54 | for state in metadata.states { 55 | states.push(IconState { 56 | name: state.name, 57 | dirs: match state.dirs { 58 | Dirs::One => 1, 59 | Dirs::Four => 4, 60 | Dirs::Eight => 8, 61 | }, 62 | frames: state.frames.count() as u32, 63 | }); 64 | } 65 | 66 | let icon_metadata = IconMetadata { 67 | width: metadata.width, 68 | height: metadata.height, 69 | states, 70 | }; 71 | 72 | return serde_json::to_string(&icon_metadata).unwrap(); 73 | } 74 | -------------------------------------------------------------------------------- /third_party/sdmmparser/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | extern crate dreammaker as dm; 3 | extern crate serde; 4 | #[macro_use] 5 | extern crate serde_derive; 6 | extern crate serde_json; 7 | 8 | use core::mem; 9 | use std::ffi::{CStr, CString}; 10 | use std::os::raw::c_char; 11 | use std::str; 12 | 13 | use environment::parse_environment; 14 | use icon::parse_icon_metadata; 15 | 16 | mod environment; 17 | mod icon; 18 | 19 | #[no_mangle] 20 | #[allow(non_snake_case)] 21 | pub extern fn SdmmParseEnvironment(native_path: *const c_char) -> *const c_char { 22 | to_ptr(parse_environment(to_string(native_path))) 23 | } 24 | 25 | #[no_mangle] 26 | #[allow(non_snake_case)] 27 | pub extern fn SdmmParseIconMetadata(native_path: *const c_char) -> *const c_char { 28 | to_ptr(parse_icon_metadata(to_string(native_path))) 29 | } 30 | 31 | #[no_mangle] 32 | #[allow(non_snake_case)] 33 | pub extern fn SdmmFreeStr(native_str: *mut c_char) { 34 | unsafe { let _ = CString::from_raw(native_str); }; 35 | } 36 | 37 | /// Convert a native string to a Rust string 38 | fn to_string(pointer: *const c_char) -> String { 39 | let slice = unsafe { CStr::from_ptr(pointer).to_bytes() }; 40 | str::from_utf8(slice).unwrap().to_string() 41 | } 42 | 43 | /// Convert a Rust string to a native string 44 | fn to_ptr(string: String) -> *const c_char { 45 | let cs = CString::new(string.as_bytes()).unwrap(); 46 | let ptr = cs.as_ptr(); 47 | // Tell Rust not to clean up the string while we still have a pointer to it. 48 | // Otherwise, we'll get a segfault. 49 | mem::forget(cs); 50 | ptr 51 | } 52 | -------------------------------------------------------------------------------- /versioninfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "FixedFileInfo": { 3 | "FileVersion": { 4 | "Major": 1, 5 | "Minor": 0, 6 | "Patch": 0, 7 | "Build": 0 8 | }, 9 | "ProductVersion": { 10 | "Major": 1, 11 | "Minor": 0, 12 | "Patch": 0, 13 | "Build": 0 14 | }, 15 | "FileFlagsMask": "3f", 16 | "FileFlags ": "00", 17 | "FileOS": "040004", 18 | "FileType": "01", 19 | "FileSubType": "00" 20 | }, 21 | "StringFileInfo": { 22 | "Comments": "", 23 | "CompanyName": "", 24 | "FileDescription": "StrongDMM", 25 | "FileVersion": "", 26 | "InternalName": "", 27 | "LegalCopyright": "Copyright (C) 2019-2024", 28 | "LegalTrademarks": "", 29 | "OriginalFilename": "", 30 | "PrivateBuild": "", 31 | "ProductName": "StrongDMM", 32 | "ProductVersion": "v1.0.0.0", 33 | "SpecialBuild": "" 34 | }, 35 | "VarFileInfo": { 36 | "Translation": { 37 | "LangID": "0409", 38 | "CharsetID": "04B0" 39 | } 40 | }, 41 | "IconPath": "internal/rsc/icon.ico", 42 | "ManifestPath": "" 43 | } 44 | --------------------------------------------------------------------------------