diff --git a/hswidget/animdatawidget/doc.go b/pkg/widget/animdatawidget/doc.go similarity index 100% rename from hswidget/animdatawidget/doc.go rename to pkg/widget/animdatawidget/doc.go diff --git a/hswidget/animdatawidget/state.go b/pkg/widget/animdatawidget/state.go similarity index 100% rename from hswidget/animdatawidget/state.go rename to pkg/widget/animdatawidget/state.go diff --git a/hswidget/animdatawidget/widget.go b/pkg/widget/animdatawidget/widget.go similarity index 98% rename from hswidget/animdatawidget/widget.go rename to pkg/widget/animdatawidget/widget.go index 492a2f85..ad967b93 100644 --- a/hswidget/animdatawidget/widget.go +++ b/pkg/widget/animdatawidget/widget.go @@ -12,7 +12,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2animdata" "github.com/gucio321/HellSpawner/pkg/common" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) const ( @@ -82,7 +82,7 @@ func (p *widget) buildAnimationsList() { for idx, name := range keys { currentIdx := idx list[idx] = giu.Row( - hswidget.MakeImageButton( + widget.MakeImageButton( "##"+p.id+"deleteEntry"+giu.ID(strconv.Itoa(currentIdx)), imageButtonSize, imageButtonSize, state.deleteIcon, diff --git a/hswidget/cofwidget/doc.go b/pkg/widget/cofwidget/doc.go similarity index 100% rename from hswidget/cofwidget/doc.go rename to pkg/widget/cofwidget/doc.go diff --git a/hswidget/cofwidget/helpers.go b/pkg/widget/cofwidget/helpers.go similarity index 100% rename from hswidget/cofwidget/helpers.go rename to pkg/widget/cofwidget/helpers.go diff --git a/hswidget/cofwidget/state.go b/pkg/widget/cofwidget/state.go similarity index 94% rename from hswidget/cofwidget/state.go rename to pkg/widget/cofwidget/state.go index 77c254c9..08757cc3 100644 --- a/hswidget/cofwidget/state.go +++ b/pkg/widget/cofwidget/state.go @@ -9,7 +9,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" "github.com/gucio321/HellSpawner/pkg/assets" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) type mode int32 @@ -46,7 +46,7 @@ type viewerState struct { DirectionIndex int32 FrameIndex int32 layer *d2cof.CofLayer - confirmDialog *hswidget.PopUpConfirmDialog + confirmDialog *widget.PopUpConfirmDialog } // Dispose clears viewer's layers @@ -100,7 +100,7 @@ func (p *widget) initState() { state := &widgetState{ Mode: modeViewer, viewerState: &viewerState{ - confirmDialog: &hswidget.PopUpConfirmDialog{}, + confirmDialog: &widget.PopUpConfirmDialog{}, }, newLayerFields: &newLayerFields{ Selectable: true, diff --git a/hswidget/cofwidget/widget.go b/pkg/widget/cofwidget/widget.go similarity index 94% rename from hswidget/cofwidget/widget.go rename to pkg/widget/cofwidget/widget.go index 1653bdd5..980dc19a 100644 --- a/hswidget/cofwidget/widget.go +++ b/pkg/widget/cofwidget/widget.go @@ -12,7 +12,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" "github.com/gucio321/HellSpawner/pkg/common" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) const ( @@ -108,7 +108,7 @@ func (p *widget) makeAnimationTab(state *widgetState) giu.Layout { } speedLabel := giu.Label(strSpeed) - speedInput := hswidget.MakeInputInt( + speedInput := widget.MakeInputInt( speedInputW, &p.cof.Speed, setSpeed, @@ -161,7 +161,7 @@ func (p *widget) makeLayerTab(state *widgetState) giu.Layout { } id := giu.ID(fmt.Sprintf("##%sDeleteLayerConfirm", p.id)) - state.viewerState.confirmDialog = hswidget.NewPopUpConfirmDialog(id, strPrompt, strMessage, fnYes, fnNo) + state.viewerState.confirmDialog = widget.NewPopUpConfirmDialog(id, strPrompt, strMessage, fnYes, fnNo) state.Mode = modeConfirm }) @@ -236,7 +236,7 @@ func (p *widget) makePriorityTab(state *widgetState) giu.Layout { popupID := giu.ID(fmt.Sprintf("%sDeleteLayerConfirm", p.id)) - state.confirmDialog = hswidget.NewPopUpConfirmDialog(popupID, strPrompt, strMessage, fnYes, fnNo) + state.confirmDialog = widget.NewPopUpConfirmDialog(popupID, strPrompt, strMessage, fnYes, fnNo) state.Mode = modeConfirm }) @@ -277,9 +277,9 @@ func (p *widget) layoutAnimFrames(state *widgetState) *giu.RowWidget { leftButtonID := giu.ID(fmt.Sprintf("##%sDecreaseFramesPerDirection", p.id)) rightButtonID := giu.ID(fmt.Sprintf("##%sIncreaseFramesPerDirection", p.id)) - left := hswidget.MakeImageButton(giu.ID(leftButtonID), buttonWidthHeight, buttonWidthHeight, state.textures.left, fnDecrease) + left := widget.MakeImageButton(giu.ID(leftButtonID), buttonWidthHeight, buttonWidthHeight, state.textures.left, fnDecrease) frameCount := giu.Label(fmt.Sprintf("%d", numFrames)) - right := hswidget.MakeImageButton(rightButtonID, buttonWidthHeight, buttonWidthHeight, state.textures.right, fnIncrease) + right := widget.MakeImageButton(rightButtonID, buttonWidthHeight, buttonWidthHeight, state.textures.right, fnIncrease) return giu.Row(label, left, frameCount, right) } @@ -374,8 +374,8 @@ func (p *widget) makeDirectionLayout() giu.Layout { fnIncPriority := makeIncPriorityFn(currentIdx) fnDecPriority := makeDecPriorityFn(currentIdx) - increasePriority := hswidget.MakeImageButton(strIncPri, buttonWidthHeight, buttonWidthHeight, state.textures.up, fnIncPriority) - decreasePriority := hswidget.MakeImageButton(strDecPri, buttonWidthHeight, buttonWidthHeight, state.textures.down, fnDecPriority) + increasePriority := widget.MakeImageButton(strIncPri, buttonWidthHeight, buttonWidthHeight, state.textures.up, fnIncPriority) + decreasePriority := widget.MakeImageButton(strDecPri, buttonWidthHeight, buttonWidthHeight, state.textures.down, fnDecPriority) strLayerName := layers[idx].Name() strLayerLabel := fmt.Sprintf(fmtLayerLabel, idx, strLayerName) @@ -447,7 +447,7 @@ func (p *widget) makeAddLayerLayout() giu.Layout { ), giu.Row( giu.Label("Shadow: "), - hswidget.MakeCheckboxFromByte("##"+p.id+"AddLayerShadow", &state.newLayerFields.Shadow), + widget.MakeCheckboxFromByte("##"+p.id+"AddLayerShadow", &state.newLayerFields.Shadow), ), giu.Row( giu.Label("Selectable: "), diff --git a/hswidget/custom_widgets.go b/pkg/widget/custom_widgets.go similarity index 99% rename from hswidget/custom_widgets.go rename to pkg/widget/custom_widgets.go index 0e408a3d..24d98048 100644 --- a/hswidget/custom_widgets.go +++ b/pkg/widget/custom_widgets.go @@ -1,4 +1,4 @@ -package hswidget +package widget import ( "fmt" diff --git a/hswidget/dc6widget/doc.go b/pkg/widget/dc6widget/doc.go similarity index 100% rename from hswidget/dc6widget/doc.go rename to pkg/widget/dc6widget/doc.go diff --git a/hswidget/dc6widget/state.go b/pkg/widget/dc6widget/state.go similarity index 100% rename from hswidget/dc6widget/state.go rename to pkg/widget/dc6widget/state.go diff --git a/hswidget/dc6widget/widget.go b/pkg/widget/dc6widget/widget.go similarity index 97% rename from hswidget/dc6widget/widget.go rename to pkg/widget/dc6widget/widget.go index 61d037d4..bbb47c4e 100644 --- a/hswidget/dc6widget/widget.go +++ b/pkg/widget/dc6widget/widget.go @@ -15,7 +15,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsutil" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) const ( @@ -162,7 +162,7 @@ func (p *widget) makePlayerLayout(state *widgetState) giu.Layout { giu.InputInt(&state.TickTime).Label("Tick time").Size(inputIntW).OnChange(func() { state.ticker.Reset(time.Second * time.Duration(state.TickTime) / miliseconds) }), - hswidget.PlayPauseButton("##"+p.id+"PlayPauseAnimation", &state.IsPlaying, p.textureLoader). + widget.PlayPauseButton("##"+p.id+"PlayPauseAnimation", &state.IsPlaying, p.textureLoader). Size(playPauseButtonSize, playPauseButtonSize), giu.Button("Export GIF##"+p.id+"exportGif").OnClick(func() { err := p.exportGif(state) diff --git a/hswidget/dccwidget/doc.go b/pkg/widget/dccwidget/doc.go similarity index 100% rename from hswidget/dccwidget/doc.go rename to pkg/widget/dccwidget/doc.go diff --git a/hswidget/dccwidget/state.go b/pkg/widget/dccwidget/state.go similarity index 100% rename from hswidget/dccwidget/state.go rename to pkg/widget/dccwidget/state.go diff --git a/hswidget/dccwidget/widget.go b/pkg/widget/dccwidget/widget.go similarity index 96% rename from hswidget/dccwidget/widget.go rename to pkg/widget/dccwidget/widget.go index 1e205ed4..078772b1 100644 --- a/hswidget/dccwidget/widget.go +++ b/pkg/widget/dccwidget/widget.go @@ -15,7 +15,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsutil" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) const ( @@ -144,7 +144,7 @@ func (p *widget) makePlayerLayout(state *widgetState) giu.Layout { giu.InputInt(&state.TickTime).Label("Tick time").Size(inputIntW).OnChange(func() { state.ticker.Reset(time.Second * time.Duration(state.TickTime) / miliseconds) }), - hswidget.PlayPauseButton("##"+p.id+"PlayPauseAnimation", &state.IsPlaying, p.textureLoader). + widget.PlayPauseButton("##"+p.id+"PlayPauseAnimation", &state.IsPlaying, p.textureLoader). Size(playPauseButtonSize, playPauseButtonSize), giu.Button("Export GIF##"+p.id+"exportGif").OnClick(func() { err := p.exportGif(state) diff --git a/hswidget/doc.go b/pkg/widget/doc.go similarity index 87% rename from hswidget/doc.go rename to pkg/widget/doc.go index 0088f4d6..7cf0f324 100644 --- a/hswidget/doc.go +++ b/pkg/widget/doc.go @@ -1,2 +1,2 @@ // Package hswidget contains a generic editor widget implementation, along with with concrete editor implementations. -package hswidget +package widget diff --git a/hswidget/ds1widget/doc.go b/pkg/widget/ds1widget/doc.go similarity index 100% rename from hswidget/ds1widget/doc.go rename to pkg/widget/ds1widget/doc.go diff --git a/hswidget/ds1widget/state.go b/pkg/widget/ds1widget/state.go similarity index 95% rename from hswidget/ds1widget/state.go rename to pkg/widget/ds1widget/state.go index 42792eec..3ce5f30e 100644 --- a/hswidget/ds1widget/state.go +++ b/pkg/widget/ds1widget/state.go @@ -6,7 +6,7 @@ import ( "github.com/AllenDang/giu" "github.com/gucio321/HellSpawner/pkg/assets" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) type widgetMode int32 @@ -59,7 +59,7 @@ func (t *ds1AddPathState) Dispose() { type widgetState struct { *ds1Controls Mode widgetMode - confirmDialog *hswidget.PopUpConfirmDialog + confirmDialog *widget.PopUpConfirmDialog NewFilePath string addObjectState ds1AddObjectState addPathState ds1AddPathState diff --git a/hswidget/ds1widget/tile_records.go b/pkg/widget/ds1widget/tile_records.go similarity index 100% rename from hswidget/ds1widget/tile_records.go rename to pkg/widget/ds1widget/tile_records.go diff --git a/hswidget/ds1widget/widget.go b/pkg/widget/ds1widget/widget.go similarity index 96% rename from hswidget/ds1widget/widget.go rename to pkg/widget/ds1widget/widget.go index 1a050246..318761ea 100644 --- a/hswidget/ds1widget/widget.go +++ b/pkg/widget/ds1widget/widget.go @@ -14,7 +14,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" "github.com/gucio321/HellSpawner/pkg/common" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) const ( @@ -110,7 +110,7 @@ func (p *widget) makeDataLayout() giu.Layout { giu.Row( giu.Label("Version: "), giu.InputInt(&version).Size(inputIntW).OnChange(func() { - state.confirmDialog = hswidget.NewPopUpConfirmDialog( + state.confirmDialog = widget.NewPopUpConfirmDialog( "##"+p.id+"confirmVersionChange", "Are you sure, you want to change DS1 Version?", "This value is used while decoding and encoding ds1 file\n"+ @@ -133,7 +133,7 @@ func (p *widget) makeDataLayout() giu.Layout { giu.Row( giu.Label("\tWidth: "), giu.InputInt(&w).Size(inputIntW).OnChange(func() { - state.confirmDialog = hswidget.NewPopUpConfirmDialog( + state.confirmDialog = widget.NewPopUpConfirmDialog( "##"+p.id+"confirmWidthChange", "Are you really sure, you want to change size of DS1 tiles?", "This will affect all your tiles in Tile tab.\n"+ @@ -152,7 +152,7 @@ func (p *widget) makeDataLayout() giu.Layout { giu.Row( giu.Label("\tHeight: "), giu.InputInt(&h).Size(inputIntW).OnChange(func() { - state.confirmDialog = hswidget.NewPopUpConfirmDialog( + state.confirmDialog = widget.NewPopUpConfirmDialog( "##"+p.id+"confirmWidthChange", "Are you really sure, you want to change size of DS1 tiles?", "This will affect all your tiles in Tile tab.\n"+ @@ -194,7 +194,7 @@ func (p *widget) makeFilesLayout() giu.Layout { l = append(l, giu.Layout{ giu.Row( - hswidget.MakeImageButton( + widget.MakeImageButton( "##"+p.id+"DeleteFile"+giu.ID(strconv.Itoa(currentIdx)), deleteButtonSize, deleteButtonSize, p.deleteButtonTexture, @@ -267,7 +267,7 @@ func (p *widget) makeObjectsLayout(state *widgetState) giu.Layout { giu.Button("").ID("Add path to this Object...##"+p.id+"AddPath").Size(actionButtonW, actionButtonH).OnClick(func() { state.Mode = widgetModeAddPath }), - hswidget.MakeImageButton( + widget.MakeImageButton( "##"+p.id+"deleteObject", layerDeleteButtonSize, layerDeleteButtonSize, p.deleteButtonTexture, @@ -297,7 +297,7 @@ func (p *widget) makeObjectLayout(state *widgetState) giu.Layout { l := giu.Layout{ giu.Row( giu.Label("Type: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &obj.Type, nil, @@ -305,7 +305,7 @@ func (p *widget) makeObjectLayout(state *widgetState) giu.Layout { ), giu.Row( giu.Label("ID: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &obj.ID, nil, @@ -314,7 +314,7 @@ func (p *widget) makeObjectLayout(state *widgetState) giu.Layout { giu.Label("Position (tiles): "), giu.Row( giu.Label("\tX: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &obj.X, nil, @@ -322,7 +322,7 @@ func (p *widget) makeObjectLayout(state *widgetState) giu.Layout { ), giu.Row( giu.Label("\tY: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &obj.Y, nil, @@ -330,7 +330,7 @@ func (p *widget) makeObjectLayout(state *widgetState) giu.Layout { ), giu.Row( giu.Label("Flags: 0x"), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &obj.Flags, nil, @@ -413,7 +413,7 @@ func (p *widget) makePathLayout(state *widgetState, obj *d2ds1.Object) giu.Layou giu.Label(fmt.Sprintf("%d", idx)), giu.Label(fmt.Sprintf("(%d, %d)", int(x), int(y))), giu.Label(fmt.Sprintf("%d", obj.Paths[idx].Action)), - hswidget.MakeImageButton( + widget.MakeImageButton( "##"+p.id+"deletePath"+giu.ID(strconv.Itoa(currentIdx)), deleteButtonSize, deleteButtonSize, p.deleteButtonTexture, @@ -518,7 +518,7 @@ func (p *widget) makeTilesGroupLayout(state *widgetState, x, y int, t d2ds1.Laye var deleteBtn giu.Widget if deleteCb != nil { - deleteBtn = hswidget.MakeImageButton( + deleteBtn = widget.MakeImageButton( "##"+p.id+"delete"+giu.ID(t.String()), layerDeleteButtonSize, layerDeleteButtonSize, p.deleteButtonTexture, @@ -583,7 +583,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. l := giu.Layout{ giu.Row( giu.Label("Prop1: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &record.Prop1, nil, @@ -591,7 +591,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. ), giu.Row( giu.Label("Sequence: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &record.Sequence, nil, @@ -599,7 +599,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. ), giu.Row( giu.Label("Unknown1: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &record.Unknown1, nil, @@ -607,7 +607,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. ), giu.Row( giu.Label("Style: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &record.Style, nil, @@ -615,7 +615,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. ), giu.Row( giu.Label("Unknown2: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &record.Unknown2, nil, @@ -623,7 +623,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. ), giu.Row( giu.Label("Hidden: "), - hswidget.MakeCheckboxFromByte( + widget.MakeCheckboxFromByte( "##"+p.id+"floorHidden", &record.HiddenBytes, ), @@ -641,7 +641,7 @@ func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu. l = append(l, giu.Row( giu.Label("Zero: "), - hswidget.MakeInputInt( + widget.MakeInputInt( inputIntW, &record.Zero, nil, diff --git a/hswidget/dt1widget/doc.go b/pkg/widget/dt1widget/doc.go similarity index 100% rename from hswidget/dt1widget/doc.go rename to pkg/widget/dt1widget/doc.go diff --git a/hswidget/dt1widget/helpers.go b/pkg/widget/dt1widget/helpers.go similarity index 100% rename from hswidget/dt1widget/helpers.go rename to pkg/widget/dt1widget/helpers.go diff --git a/hswidget/dt1widget/state.go b/pkg/widget/dt1widget/state.go similarity index 100% rename from hswidget/dt1widget/state.go rename to pkg/widget/dt1widget/state.go diff --git a/hswidget/dt1widget/sub_tile_flags.go b/pkg/widget/dt1widget/sub_tile_flags.go similarity index 100% rename from hswidget/dt1widget/sub_tile_flags.go rename to pkg/widget/dt1widget/sub_tile_flags.go diff --git a/hswidget/dt1widget/tile_type_image.go b/pkg/widget/dt1widget/tile_type_image.go similarity index 94% rename from hswidget/dt1widget/tile_type_image.go rename to pkg/widget/dt1widget/tile_type_image.go index 46cea1c7..ec281924 100644 --- a/hswidget/dt1widget/tile_type_image.go +++ b/pkg/widget/dt1widget/tile_type_image.go @@ -3,7 +3,7 @@ package dt1widget import ( "github.com/AllenDang/giu" - "github.com/gucio321/HellSpawner/hswidget/dt1widget/tiletypeimage" + "github.com/gucio321/HellSpawner/pkg/widget/dt1widget/tiletypeimage" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" ) diff --git a/hswidget/dt1widget/tiletypeimage/doc.go b/pkg/widget/dt1widget/tiletypeimage/doc.go similarity index 100% rename from hswidget/dt1widget/tiletypeimage/doc.go rename to pkg/widget/dt1widget/tiletypeimage/doc.go diff --git a/hswidget/dt1widget/tiletypeimage/tile_type_image.go b/pkg/widget/dt1widget/tiletypeimage/tile_type_image.go similarity index 100% rename from hswidget/dt1widget/tiletypeimage/tile_type_image.go rename to pkg/widget/dt1widget/tiletypeimage/tile_type_image.go diff --git a/hswidget/dt1widget/widget.go b/pkg/widget/dt1widget/widget.go similarity index 99% rename from hswidget/dt1widget/widget.go rename to pkg/widget/dt1widget/widget.go index a905e211..0c01c09f 100644 --- a/hswidget/dt1widget/widget.go +++ b/pkg/widget/dt1widget/widget.go @@ -19,7 +19,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" "github.com/gucio321/HellSpawner/pkg/common" - "github.com/gucio321/HellSpawner/hswidget/dt1widget/tiletypeimage" + "github.com/gucio321/HellSpawner/pkg/widget/dt1widget/tiletypeimage" ) const ( diff --git a/hswidget/fonttablewidget/doc.go b/pkg/widget/fonttablewidget/doc.go similarity index 100% rename from hswidget/fonttablewidget/doc.go rename to pkg/widget/fonttablewidget/doc.go diff --git a/hswidget/fonttablewidget/state.go b/pkg/widget/fonttablewidget/state.go similarity index 100% rename from hswidget/fonttablewidget/state.go rename to pkg/widget/fonttablewidget/state.go diff --git a/hswidget/fonttablewidget/widget.go b/pkg/widget/fonttablewidget/widget.go similarity index 98% rename from hswidget/fonttablewidget/widget.go rename to pkg/widget/fonttablewidget/widget.go index e33747a2..07e48115 100644 --- a/hswidget/fonttablewidget/widget.go +++ b/pkg/widget/fonttablewidget/widget.go @@ -12,7 +12,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font/d2fontglyph" "github.com/gucio321/HellSpawner/pkg/common" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widget" ) const ( @@ -124,7 +124,7 @@ func (p *widget) makeGlyphLayout(r rune) *giu.TableRowWidget { height32 := int32(h) row := giu.TableRow( - hswidget.MakeImageButton("##"+p.id+"deleteFrame"+giu.ID(r), + widget.MakeImageButton("##"+p.id+"deleteFrame"+giu.ID(r), delSize, delSize, state.deleteButtonTexture, func() { p.deleteRow(r) }, diff --git a/hswidget/palettegrideditorwidget/doc.go b/pkg/widget/palettegrideditorwidget/doc.go similarity index 100% rename from hswidget/palettegrideditorwidget/doc.go rename to pkg/widget/palettegrideditorwidget/doc.go diff --git a/hswidget/palettegrideditorwidget/helpers.go b/pkg/widget/palettegrideditorwidget/helpers.go similarity index 100% rename from hswidget/palettegrideditorwidget/helpers.go rename to pkg/widget/palettegrideditorwidget/helpers.go diff --git a/hswidget/palettegrideditorwidget/state.go b/pkg/widget/palettegrideditorwidget/state.go similarity index 100% rename from hswidget/palettegrideditorwidget/state.go rename to pkg/widget/palettegrideditorwidget/state.go diff --git a/hswidget/palettegrideditorwidget/widget.go b/pkg/widget/palettegrideditorwidget/widget.go similarity index 97% rename from hswidget/palettegrideditorwidget/widget.go rename to pkg/widget/palettegrideditorwidget/widget.go index 688c1580..db55975a 100644 --- a/hswidget/palettegrideditorwidget/widget.go +++ b/pkg/widget/palettegrideditorwidget/widget.go @@ -8,7 +8,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsutil" - "github.com/gucio321/HellSpawner/hswidget/palettegridwidget" + "github.com/gucio321/HellSpawner/pkg/widget/palettegridwidget" ) const ( diff --git a/hswidget/palettegridwidget/doc.go b/pkg/widget/palettegridwidget/doc.go similarity index 100% rename from hswidget/palettegridwidget/doc.go rename to pkg/widget/palettegridwidget/doc.go diff --git a/hswidget/palettegridwidget/palettecolor.go b/pkg/widget/palettegridwidget/palettecolor.go similarity index 100% rename from hswidget/palettegridwidget/palettecolor.go rename to pkg/widget/palettegridwidget/palettecolor.go diff --git a/hswidget/palettegridwidget/state.go b/pkg/widget/palettegridwidget/state.go similarity index 100% rename from hswidget/palettegridwidget/state.go rename to pkg/widget/palettegridwidget/state.go diff --git a/hswidget/palettegridwidget/widget.go b/pkg/widget/palettegridwidget/widget.go similarity index 100% rename from hswidget/palettegridwidget/widget.go rename to pkg/widget/palettegridwidget/widget.go diff --git a/hswidget/palettemapwidget/doc.go b/pkg/widget/palettemapwidget/doc.go similarity index 100% rename from hswidget/palettemapwidget/doc.go rename to pkg/widget/palettemapwidget/doc.go diff --git a/hswidget/palettemapwidget/enum.go b/pkg/widget/palettemapwidget/enum.go similarity index 100% rename from hswidget/palettemapwidget/enum.go rename to pkg/widget/palettemapwidget/enum.go diff --git a/hswidget/palettemapwidget/helpers.go b/pkg/widget/palettemapwidget/helpers.go similarity index 100% rename from hswidget/palettemapwidget/helpers.go rename to pkg/widget/palettemapwidget/helpers.go diff --git a/hswidget/palettemapwidget/palette_transform.go b/pkg/widget/palettemapwidget/palette_transform.go similarity index 96% rename from hswidget/palettemapwidget/palette_transform.go rename to pkg/widget/palettemapwidget/palette_transform.go index 8247a4bb..79afc265 100644 --- a/hswidget/palettemapwidget/palette_transform.go +++ b/pkg/widget/palettemapwidget/palette_transform.go @@ -7,8 +7,8 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2pl2" - "github.com/gucio321/HellSpawner/hswidget/palettegrideditorwidget" - "github.com/gucio321/HellSpawner/hswidget/palettegridwidget" + "github.com/gucio321/HellSpawner/pkg/widget/palettegrideditorwidget" + "github.com/gucio321/HellSpawner/pkg/widget/palettegridwidget" ) func (p *widget) makeGrid(key string, colors *[256]palettegridwidget.PaletteColor) { diff --git a/hswidget/palettemapwidget/state.go b/pkg/widget/palettemapwidget/state.go similarity index 100% rename from hswidget/palettemapwidget/state.go rename to pkg/widget/palettemapwidget/state.go diff --git a/hswidget/palettemapwidget/widget.go b/pkg/widget/palettemapwidget/widget.go similarity index 97% rename from hswidget/palettemapwidget/widget.go rename to pkg/widget/palettemapwidget/widget.go index 442b9529..a985cc7d 100644 --- a/hswidget/palettemapwidget/widget.go +++ b/pkg/widget/palettemapwidget/widget.go @@ -10,8 +10,8 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsutil" - "github.com/gucio321/HellSpawner/hswidget/palettegrideditorwidget" - "github.com/gucio321/HellSpawner/hswidget/palettegridwidget" + "github.com/gucio321/HellSpawner/pkg/widget/palettegrideditorwidget" + "github.com/gucio321/HellSpawner/pkg/widget/palettegridwidget" ) const ( diff --git a/hswidget/popupconfirm.go b/pkg/widget/popupconfirm.go similarity index 98% rename from hswidget/popupconfirm.go rename to pkg/widget/popupconfirm.go index edcbbb31..0588dae5 100644 --- a/hswidget/popupconfirm.go +++ b/pkg/widget/popupconfirm.go @@ -1,4 +1,4 @@ -package hswidget +package widget import ( "log" diff --git a/hswidget/selectpalettewidget/doc.go b/pkg/widget/selectpalettewidget/doc.go similarity index 100% rename from hswidget/selectpalettewidget/doc.go rename to pkg/widget/selectpalettewidget/doc.go diff --git a/hswidget/selectpalettewidget/widget.go b/pkg/widget/selectpalettewidget/widget.go similarity index 100% rename from hswidget/selectpalettewidget/widget.go rename to pkg/widget/selectpalettewidget/widget.go diff --git a/hswidget/stringtablewidget/doc.go b/pkg/widget/stringtablewidget/doc.go similarity index 100% rename from hswidget/stringtablewidget/doc.go rename to pkg/widget/stringtablewidget/doc.go diff --git a/hswidget/stringtablewidget/helpers.go b/pkg/widget/stringtablewidget/helpers.go similarity index 100% rename from hswidget/stringtablewidget/helpers.go rename to pkg/widget/stringtablewidget/helpers.go diff --git a/hswidget/stringtablewidget/state.go b/pkg/widget/stringtablewidget/state.go similarity index 100% rename from hswidget/stringtablewidget/state.go rename to pkg/widget/stringtablewidget/state.go diff --git a/hswidget/stringtablewidget/widget.go b/pkg/widget/stringtablewidget/widget.go similarity index 100% rename from hswidget/stringtablewidget/widget.go rename to pkg/widget/stringtablewidget/widget.go diff --git a/pkg/widgets/animdatawidget/doc.go b/pkg/widgets/animdatawidget/doc.go new file mode 100644 index 00000000..66023819 --- /dev/null +++ b/pkg/widgets/animdatawidget/doc.go @@ -0,0 +1,3 @@ +// Package animdatawidget provides data necessary for viewing and editing +// d2animdata.AnimationData structure +package animdatawidget diff --git a/pkg/widgets/animdatawidget/state.go b/pkg/widgets/animdatawidget/state.go new file mode 100644 index 00000000..86e54a90 --- /dev/null +++ b/pkg/widgets/animdatawidget/state.go @@ -0,0 +1,85 @@ +package animdatawidget + +import ( + "fmt" + "sort" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/assets" +) + +type widgetMode int32 + +const ( + widgetModeList widgetMode = iota + widgetModeViewRecord +) + +type widgetState struct { + Mode widgetMode + mapKeys []string + MapIndex int32 + RecordIdx int32 + deleteIcon *giu.Texture + addEntryState +} + +// Dispose clears widget's state +func (s *widgetState) Dispose() { + s.Mode = widgetModeList + s.mapKeys = make([]string, 0) + s.MapIndex = 0 + s.RecordIdx = 0 + s.addEntryState.Dispose() + s.deleteIcon = nil +} + +type addEntryState struct { + Name string +} + +func (s *addEntryState) Dispose() { + s.Name = "" +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) initState() { + state := &widgetState{} + + p.textureLoader.CreateTextureFromFile(assets.DeleteIcon, func(texture *giu.Texture) { + state.deleteIcon = texture + }) + + p.setState(state) + + p.reloadMapKeys() +} + +func (p *widget) reloadMapKeys() { + state := p.getState() + state.mapKeys = p.d2.GetRecordNames() + sort.Strings(state.mapKeys) +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} diff --git a/pkg/widgets/animdatawidget/widget.go b/pkg/widgets/animdatawidget/widget.go new file mode 100644 index 00000000..c297a25c --- /dev/null +++ b/pkg/widgets/animdatawidget/widget.go @@ -0,0 +1,268 @@ +package animdatawidget + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + "strings" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2animdata" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +const ( + listW, listH = 200, 400 + inputIntW = 30 + actionBtnW, actionBtnH = 200, 30 + saveCancelButtonW, saveCancelButtonH = 50, 30 +) + +type widget struct { + id giu.ID + d2 *d2animdata.AnimationData + textureLoader common.TextureLoader +} + +// Create creates a new widget +func Create(textureLoader common.TextureLoader, state []byte, id string, d2 *d2animdata.AnimationData) giu.Widget { + result := &widget{ + id: giu.ID(id), + d2: d2, + textureLoader: textureLoader, + } + + if state != nil && giu.Context.GetState(result.getStateID()) == nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding animation data widget state: %v", err) + } + + result.setState(s) + } + + return result +} + +// Build builds widget +func (p *widget) Build() { + state := p.getState() + + switch state.Mode { + case widgetModeList: + p.buildAnimationsList() + case widgetModeViewRecord: + p.buildViewRecordLayout() + } +} + +func (p *widget) buildAnimationsList() { + state := p.getState() + + keys := make([]string, 0) + + if state.Name != "" { + for _, key := range state.mapKeys { + if strings.Contains(key, state.Name) { + keys = append(keys, key) + } + } + } else { + keys = state.mapKeys + } + + list := make([]giu.Widget, len(keys)) + + const imageButtonSize = 13 + + for idx, name := range keys { + currentIdx := idx + list[idx] = giu.Row( + widgets.MakeImageButton( + "##"+p.id+"deleteEntry"+giu.ID(strconv.Itoa(currentIdx)), + imageButtonSize, imageButtonSize, + state.deleteIcon, + func() { + p.deleteEntry(state.mapKeys[currentIdx]) + }, + ), + giu.Selectable(name).OnClick(func() { + state.MapIndex = int32(currentIdx) + state.Mode = widgetModeViewRecord + }), + ) + } + + giu.Layout{ + p.makeSearchLayout(), + giu.Separator(), + giu.Child().Border(false). + Size(listW, listH). + Layout(giu.Layout{ + giu.Custom(func() { + if len(list) > 0 { + giu.Layout(list).Build() + + return + } + + giu.Label("Nothing matches...").Build() + }), + }), + }.Build() +} + +func (p *widget) buildViewRecordLayout() { + state := p.getState() + + name := state.mapKeys[state.MapIndex] + records := p.d2.GetRecords(name) + record := records[state.RecordIdx] + + max := len(records) - 1 + + fpd := int32(record.FramesPerDirection()) + speed := int32(record.Speed()) + + giu.Layout{ + giu.Row( + giu.ArrowButton(giu.DirectionLeft). + ID("##"+p.id+"previousAnimation").OnClick(func() { + state.RecordIdx = 0 + + if state.MapIndex > 0 { + state.MapIndex-- + } + }), + giu.Label(fmt.Sprintf("Animation name: %s", name)), + giu.ArrowButton(giu.DirectionRight). + ID("##"+p.id+"nextAnimation").OnClick(func() { + state.RecordIdx = 0 + + if int(state.MapIndex) < len(state.mapKeys)-1 { + state.MapIndex++ + } + }), + ), + giu.Separator(), + giu.Custom(func() { + if max > 0 { + giu.Layout{ + giu.SliderInt(&state.RecordIdx, 0, int32(max)).ID("record##" + p.id), + giu.Separator(), + }.Build() + } + }), + giu.Row( + giu.Label("Frames per direction: "), + giu.InputInt(&fpd).Size(inputIntW).OnChange(func() { + record.SetFramesPerDirection(uint32(fpd)) + }), + ), + giu.Row( + giu.Label("Speed: "), + giu.InputInt(&speed).Size(inputIntW).OnChange(func() { + record.SetSpeed(uint16(speed)) + }), + ), + giu.Label(fmt.Sprintf("FPS: %v", record.FPS())), + giu.Label(fmt.Sprintf("Frame duration: %v (miliseconds)", record.FrameDurationMS())), + giu.Separator(), + giu.Button("").ID("Back to entry preview##"+p.id+"backToRecordSelection").Size(actionBtnW, actionBtnH).OnClick(func() { + state.Mode = widgetModeList + }), + giu.Button("").ID("Add record##"+p.id+"addRecordBtn").Size(actionBtnW, actionBtnH).OnClick(func() { + p.d2.PushRecord(name) + + // no -1, because current records hasn't new field yet + state.RecordIdx = int32(len(records)) + }), + giu.Button("").ID("Delete record##"+p.id+"deleteRecordBtn").Size(actionBtnW, actionBtnH).OnClick(func() { + if len(records) == 1 { + state.RecordIdx = 0 + state.Mode = widgetModeList + p.deleteEntry(name) + + return + } + if state.RecordIdx == int32(len(records)-1) { + if state.RecordIdx > 0 { + state.RecordIdx-- + } else { + state.Mode = widgetModeList + } + } + + err := p.d2.DeleteRecord(name, int(state.RecordIdx)) + if err != nil { + log.Print(err) + } + }), + }.Build() +} + +func (p *widget) makeSearchLayout() giu.Layout { + state := p.getState() + + return giu.Layout{ + giu.Label("Search or type new entry name:"), + giu.InputText(&state.Name).Size(listW).OnChange(func() { + // formatting + state.Name = strings.ToUpper(state.Name) + state.Name = strings.ReplaceAll(state.Name, " ", "") + }), + giu.Custom(func() { + if state.Name == "" { + return + } + + found := (len(p.d2.GetRecords(state.Name)) > 0) + if found { + giu.Row( + giu.Button("").ID("View##"+p.id+"addEntryViewEntry").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + p.viewRecord() + }), + ).Build() + + return + } + + giu.Row( + giu.Button("").ID("Add##"+p.id+"addEntry").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + err := p.d2.AddEntry(state.Name) + if err != nil { + log.Print(err) + } + + p.d2.PushRecord(state.Name) + p.reloadMapKeys() + p.viewRecord() + }), + ).Build() + }), + } +} + +func (p *widget) viewRecord() { + state := p.getState() + + for n, i := range state.mapKeys { + if i == state.Name { + state.MapIndex = int32(n) + } + } + + state.Mode = widgetModeViewRecord +} + +func (p *widget) deleteEntry(name string) { + if err := p.d2.DeleteEntry(name); err != nil { + log.Print(fmt.Errorf("deleting entry: %w", err)) + } + + p.reloadMapKeys() +} diff --git a/pkg/widgets/cofwidget/doc.go b/pkg/widgets/cofwidget/doc.go new file mode 100644 index 00000000..030820de --- /dev/null +++ b/pkg/widgets/cofwidget/doc.go @@ -0,0 +1,3 @@ +// Package cofwidget provides a giu.Widget implementation for viewing and editing +// the COF data structure. +package cofwidget diff --git a/pkg/widgets/cofwidget/helpers.go b/pkg/widgets/cofwidget/helpers.go new file mode 100644 index 00000000..6c2f1940 --- /dev/null +++ b/pkg/widgets/cofwidget/helpers.go @@ -0,0 +1,39 @@ +package cofwidget + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" +) + +// this likely needs to be a method of d2cof.COF +func speedToFPS(speed int) float64 { + const ( + baseFPS = 25 + speedDivisor = 256 + ) + + fps := baseFPS * (float64(speed) / speedDivisor) + if fps == 0 { + fps = baseFPS + } + + return fps +} + +// this should also probably be a method of COF +func calculateDuration(cof *d2cof.COF) float64 { + const ( + milliseconds = 1000 + ) + + frameDelay := milliseconds / speedToFPS(cof.Speed) + + return float64(cof.FramesPerDirection) * frameDelay +} + +func max(a, b int) int { + if a > b { + return a + } + + return b +} diff --git a/pkg/widgets/cofwidget/state.go b/pkg/widgets/cofwidget/state.go new file mode 100644 index 00000000..eb1e83f4 --- /dev/null +++ b/pkg/widgets/cofwidget/state.go @@ -0,0 +1,132 @@ +package cofwidget + +import ( + "fmt" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" + + "github.com/gucio321/HellSpawner/pkg/assets" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +type mode int32 + +const ( + modeViewer mode = iota + modeAddLayer + modeConfirm +) + +type widgetState struct { + *viewerState + *newLayerFields + Mode mode + textures +} + +type textures struct { + up *giu.Texture + down *giu.Texture + left *giu.Texture + right *giu.Texture +} + +// Dispose clear widget's state +func (s *widgetState) Dispose() { + s.viewerState.Dispose() + s.newLayerFields.Dispose() +} + +// viewerState represents cof viewer's state +type viewerState struct { + LayerIndex int32 + DirectionIndex int32 + FrameIndex int32 + layer *d2cof.CofLayer + confirmDialog *widgets.PopUpConfirmDialog +} + +// Dispose clears viewer's layers +func (s *viewerState) Dispose() { + s.layer = nil +} + +type newLayerFields struct { + LayerType int32 + Shadow byte + Selectable bool + Transparent bool + DrawEffect int32 + WeaponClass int32 +} + +// Dispose disposes editor's state +func (s *newLayerFields) Dispose() { + s.LayerType = 0 + s.DrawEffect = 0 + s.WeaponClass = 0 +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + if len(p.cof.CofLayers) > 0 { + state.viewerState.layer = &p.cof.CofLayers[state.viewerState.LayerIndex] + } + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} + +func (p *widget) initState() { + state := &widgetState{ + Mode: modeViewer, + viewerState: &viewerState{ + confirmDialog: &widgets.PopUpConfirmDialog{}, + }, + newLayerFields: &newLayerFields{ + Selectable: true, + DrawEffect: int32(d2enum.DrawEffectNone), + }, + } + + if len(p.cof.CofLayers) > 0 { + state.viewerState.layer = &p.cof.CofLayers[0] + } + + p.textureLoader.CreateTextureFromFile(assets.UpArrowIcon, func(texture *giu.Texture) { + state.textures.up = texture + }) + + p.textureLoader.CreateTextureFromFile(assets.DownArrowIcon, func(texture *giu.Texture) { + state.textures.down = texture + }) + + p.textureLoader.CreateTextureFromFile(assets.LeftArrowIcon, func(texture *giu.Texture) { + state.textures.left = texture + }) + + p.textureLoader.CreateTextureFromFile(assets.RightArrowIcon, func(texture *giu.Texture) { + state.textures.right = texture + }) + + p.setState(state) +} diff --git a/pkg/widgets/cofwidget/widget.go b/pkg/widgets/cofwidget/widget.go new file mode 100644 index 00000000..459d87a4 --- /dev/null +++ b/pkg/widgets/cofwidget/widget.go @@ -0,0 +1,581 @@ +package cofwidget + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2cof" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +const ( + layerListW = 64 + buttonWidthHeight = 15 + actionButtonW, actionButtonH = 200, 30 + saveCancelButtonW, saveCancelButtonH = 80, 30 + bigListW = 200 + speedInputW = 40 +) + +type widget struct { + id giu.ID + cof *d2cof.COF + textureLoader common.TextureLoader +} + +// Create a new COF widget +func Create( + state []byte, + textureLoader common.TextureLoader, + id string, cof *d2cof.COF, +) giu.Widget { + result := &widget{ + id: giu.ID(id), + cof: cof, + textureLoader: textureLoader, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding cof widget state: %v", err) + } + + result.setState(s) + } + + return result +} + +// Build builds a cof viewer +func (p *widget) Build() { + state := p.getState() + + // builds appropriate menu (depends on state) + switch state.Mode { + case modeViewer: + p.makeViewerLayout().Build() + case modeAddLayer: + p.makeAddLayerLayout().Build() + case modeConfirm: + giu.Layout{ + giu.Label("Please confirm your decision"), + state.confirmDialog, + }.Build() + } +} + +func (p *widget) makeViewerLayout() giu.Layout { + state := p.getState() + + return giu.Layout{ + giu.TabBar().TabItems( + giu.TabItem("Animation").Layout(p.makeAnimationTab(state)), + giu.TabItem("Layer").Layout(p.makeLayerTab(state)), + giu.TabItem("Priority").Layout(p.makePriorityTab(state)), + ), + } +} + +func (p *widget) makeAnimationTab(state *widgetState) giu.Layout { + const ( + fmtFPS = "FPS: %.1f" + fmtDuration = "Duration: %.2fms" + fmtDirections = "Directions: %v" + strSpeed = "Speed: " + maxSpeed = 256 + ) + + numDirs := p.cof.NumberOfDirections + fps := speedToFPS(p.cof.Speed) + duration := calculateDuration(p.cof) + + strLabelDirections := fmt.Sprintf(fmtDirections, numDirs) + strLabelFPS := fmt.Sprintf(fmtFPS, fps) + strLabelDuration := fmt.Sprintf(fmtDuration, duration) + + setSpeed := func() { + if p.cof.Speed >= maxSpeed { + p.cof.Speed = maxSpeed + } + } + + speedLabel := giu.Label(strSpeed) + speedInput := widgets.MakeInputInt( + speedInputW, + &p.cof.Speed, + setSpeed, + ) + + return giu.Layout{ + giu.Label(strLabelDirections), + p.layoutAnimFrames(state), + giu.Row(speedLabel, speedInput), + giu.Label(strLabelFPS), + giu.Label(strLabelDuration), + } +} + +func (p *widget) makeLayerTab(state *widgetState) giu.Layout { + addLayerButtonID := fmt.Sprintf("Add a new layer...##%sAddLayer", p.id) + addLayerButton := giu.Button(addLayerButtonID).Size(actionButtonW, actionButtonH) + addLayerButton.OnClick(func() { + p.createNewLayer() + }) + + if state.viewerState.layer == nil { + return giu.Layout{addLayerButton} + } + + layerStrings := make([]string, 0) + for idx := range p.cof.CofLayers { + layerStrings = append(layerStrings, strconv.Itoa(int(p.cof.CofLayers[idx].Type))) + } + + currentLayerName := layerStrings[state.viewerState.LayerIndex] + layerList := giu.Combo("", currentLayerName, layerStrings, &state.LayerIndex).ID("##" + p.id + "layer") + layerList.Size(layerListW).OnChange(p.onUpdate) + + deleteLayerButtonID := fmt.Sprintf("Delete current layer...##%sDeleteLayer", p.id) + deleteLayerButton := giu.Button(deleteLayerButtonID).Size(actionButtonW, actionButtonH) + deleteLayerButton.OnClick(func() { + const ( + strPrompt = "Do you really want to remove this layer?" + strMessage = "If you'll click YES, all data from this layer will be lost. Continue?" + ) + + fnYes := func() { + p.deleteCurrentLayer(state.viewerState.LayerIndex) + state.Mode = modeViewer + } + + fnNo := func() { + state.Mode = modeViewer + } + + id := giu.ID(fmt.Sprintf("##%sDeleteLayerConfirm", p.id)) + state.viewerState.confirmDialog = widgets.NewPopUpConfirmDialog(id, strPrompt, strMessage, fnYes, fnNo) + + state.Mode = modeConfirm + }) + + layout := giu.Layout{ + giu.Row(giu.Label("Selected Layer: "), layerList), + giu.Separator(), + p.makeLayerLayout(), + giu.Separator(), + addLayerButton, + deleteLayerButton, + } + + return layout +} + +func (p *widget) createNewLayer() { + state := p.getState() + + state.Mode = modeAddLayer +} + +func (p *widget) makePriorityTab(state *widgetState) giu.Layout { + if len(p.cof.Priority) == 0 { + return giu.Layout{ + giu.Label("Nothing here"), + } + } + + directionStrings := make([]string, 0) + for idx := range p.cof.Priority { + directionStrings = append(directionStrings, fmt.Sprintf("%d", idx)) + } + + directionString := directionStrings[state.viewerState.DirectionIndex] + directionList := giu.Combo("", directionString, directionStrings, &state.DirectionIndex). + ID("##" + p.id + "dir") + directionList.Size(layerListW).OnChange(p.onUpdate) + + frameStrings := make([]string, 0) + for idx := range p.cof.Priority[state.DirectionIndex] { + frameStrings = append(frameStrings, fmt.Sprintf("%d", idx)) + } + + frameString := frameStrings[state.FrameIndex] + frameList := giu.Combo("", frameString, frameStrings, &state.FrameIndex). + ID("##" + p.id + "frame") + frameList.Size(layerListW).OnChange(p.onUpdate) + + const ( + strPrompt = "Do you really want to remove this direction?" + strMessage = "If you'll click YES, all data from this direction will be lost. Continue?" + ) + + duplicateButtonID := fmt.Sprintf("Duplicate current direction...##%sDuplicateDirection", p.id) + duplicateButton := giu.Button(duplicateButtonID).Size(actionButtonW, actionButtonH) + duplicateButton.OnClick(func() { + p.duplicateDirection() + }) + + deleteButtonID := fmt.Sprintf("Delete current direction...##%sDeleteDirection", p.id) + deleteButton := giu.Button(deleteButtonID).Size(actionButtonW, actionButtonH) + deleteButton.OnClick(func() { + fnYes := func() { + p.deleteCurrentDirection() + state.Mode = modeViewer + } + + fnNo := func() { + state.Mode = modeViewer + } + + popupID := giu.ID(fmt.Sprintf("%sDeleteLayerConfirm", p.id)) + + state.confirmDialog = widgets.NewPopUpConfirmDialog(popupID, strPrompt, strMessage, fnYes, fnNo) + state.Mode = modeConfirm + }) + + return giu.Layout{ + giu.Row( + giu.Label("Direction: "), directionList, + giu.Label("Frame: "), frameList, + ), + giu.Separator(), + p.makeDirectionLayout(), + duplicateButton, + deleteButton, + } +} + +// the layout ends up looking like this: +// Frames (x6): <- 10 -> +// you use the arrows to set the number of frames per direction +func (p *widget) layoutAnimFrames(state *widgetState) *giu.RowWidget { + numFrames := p.cof.FramesPerDirection + numDirs := p.cof.NumberOfDirections + + strLabel := "Frames:" + if numDirs > 1 { + strLabel = fmt.Sprintf("Frames (x%v):", numDirs) + } + + fnDecrease := func() { + p.cof.FramesPerDirection = max(p.cof.FramesPerDirection-1, 0) + } + + fnIncrease := func() { + p.cof.FramesPerDirection++ + } + + label := giu.Label(strLabel) + + leftButtonID := giu.ID(fmt.Sprintf("##%sDecreaseFramesPerDirection", p.id)) + rightButtonID := giu.ID(fmt.Sprintf("##%sIncreaseFramesPerDirection", p.id)) + + left := widgets.MakeImageButton(giu.ID(leftButtonID), buttonWidthHeight, buttonWidthHeight, state.textures.left, fnDecrease) + frameCount := giu.Label(fmt.Sprintf("%d", numFrames)) + right := widgets.MakeImageButton(rightButtonID, buttonWidthHeight, buttonWidthHeight, state.textures.right, fnIncrease) + + return giu.Row(label, left, frameCount, right) +} + +func (p *widget) onUpdate() { + state := p.getState() + + clone := p.cof.CofLayers[state.LayerIndex] + state.viewerState.layer = &clone + + giu.Context.SetState(p.id, state) +} + +func (p *widget) makeLayerLayout() giu.Layout { + state := p.getState() + + if state.viewerState.layer == nil { + p.onUpdate() + } + + layerName := state.viewerState.layer.Type.Name() + + strType := fmt.Sprintf("Type: %s (%s)", state.viewerState.layer.Type, layerName) + strShadow := fmt.Sprintf("Shadow: %t", state.viewerState.layer.Shadow > 0) + strSelectable := fmt.Sprintf("Selectable: %t", state.viewerState.layer.Selectable) + strTransparent := fmt.Sprintf("Transparent: %t", state.viewerState.layer.Transparent) + + effect := state.viewerState.layer.DrawEffect.String() + + strEffect := fmt.Sprintf("Draw Effect: %s", effect) + + weapon := state.viewerState.layer.WeaponClass.String() + + strWeaponClass := fmt.Sprintf("Weapon Class: (%s) %s", state.viewerState.layer.WeaponClass, weapon) + + return giu.Layout{ + giu.Label(strType), + giu.Label(strShadow), + giu.Label(strSelectable), + giu.Label(strTransparent), + giu.Label(strEffect), + giu.Label(strWeaponClass), + } +} + +func (p *widget) makeDirectionLayout() giu.Layout { + const ( + strRenderOrderLabel = "Render Order (first to last):" + fmtIncreasePriority = "LayerPriorityUp_%d" + fmtDecreasePriority = "LayerPriorityDown_%d" + fmtLayerLabel = "%d: %s" + ) + + state := p.getState() + + frames := p.cof.Priority[state.DirectionIndex] + layers := frames[int(state.FrameIndex)%len(frames)] + + // increase / decrease callback function providers, based on layer index + makeIncPriorityFn := func(idx int) func() { + return func() { + if idx <= 0 { + return + } + + list := &p.cof.Priority[state.DirectionIndex][state.FrameIndex] + (*list)[idx-1], (*list)[idx] = (*list)[idx], (*list)[idx-1] + } + } + + makeDecPriorityFn := func(idx int) func() { + return func() { + list := &p.cof.Priority[state.DirectionIndex][state.FrameIndex] + + if idx >= len(*list)-1 { + return + } + + (*list)[idx], (*list)[idx+1] = (*list)[idx+1], (*list)[idx] + } + } + + // each layer line looks like: + // <- -> 0: Name + // the left/right buttons use the callbacks created by the previous funcs for index=0 + buildLayerPriorityRow := func(idx int) { + currentIdx := idx + + strIncPri := giu.ID(fmt.Sprintf(fmtIncreasePriority, currentIdx)) + strDecPri := giu.ID(fmt.Sprintf(fmtDecreasePriority, currentIdx)) + + fnIncPriority := makeIncPriorityFn(currentIdx) + fnDecPriority := makeDecPriorityFn(currentIdx) + + increasePriority := widgets.MakeImageButton(strIncPri, buttonWidthHeight, buttonWidthHeight, state.textures.up, fnIncPriority) + decreasePriority := widgets.MakeImageButton(strDecPri, buttonWidthHeight, buttonWidthHeight, state.textures.down, fnDecPriority) + + strLayerName := layers[idx].Name() + strLayerLabel := fmt.Sprintf(fmtLayerLabel, idx, strLayerName) + + layerNameLabel := giu.Label(strLayerLabel) + + giu.Row(increasePriority, decreasePriority, layerNameLabel).Build() + } + + // finally, a func that we can pass to giu.Custom + buildLayerRows := func() { + for idx := range layers { + buildLayerPriorityRow(idx) + } + } + + return giu.Layout{ + giu.Label(strRenderOrderLabel), + giu.Custom(buildLayerRows), + } +} + +func (p *widget) makeAddLayerLayout() giu.Layout { + state := p.getState() + + // available is a list of available (not currently used) composite types + available := make([]d2enum.CompositeType, 0) + + for i := d2enum.CompositeTypeHead; i < d2enum.CompositeTypeMax; i++ { + contains := false + + for _, j := range p.cof.CofLayers { + if i == j.Type { + contains = true + + break + } + } + + if !contains { + available = append(available, i) + } + } + + compositeTypeList := make([]string, len(available)) + for n, i := range available { + compositeTypeList[n] = i.String() + " (" + i.Name() + ")" + } + + drawEffectList := make([]string, d2enum.DrawEffectNone+1) + for i := d2enum.DrawEffectPctTransparency25; i <= d2enum.DrawEffectNone; i++ { + drawEffectList[i] = strconv.Itoa(int(i)) + " (" + i.String() + ")" + } + + weaponClassList := make([]string, d2enum.WeaponClassTwoHandToHand+1) + for i := d2enum.WeaponClassNone; i <= d2enum.WeaponClassTwoHandToHand; i++ { + weaponClassList[i] = i.String() + " (" + i.Name() + ")" + } + + return giu.Layout{ + giu.Label("Select new COF's Layer parameters:"), + giu.Separator(), + giu.Row( + giu.Label("Type: "), + giu.Combo("", compositeTypeList[state.newLayerFields.LayerType], + compositeTypeList, &state.newLayerFields.LayerType).Size(bigListW).ID( + "##"+p.id+"AddLayerType", + ), + ), + giu.Row( + giu.Label("Shadow: "), + widgets.MakeCheckboxFromByte("##"+p.id+"AddLayerShadow", &state.newLayerFields.Shadow), + ), + giu.Row( + giu.Label("Selectable: "), + giu.Checkbox("", &state.newLayerFields.Selectable).ID("##"+p.id+"AddLayerSelectable"), + ), + giu.Row( + giu.Label("Transparent: "), + giu.Checkbox("", &state.newLayerFields.Transparent).ID("##"+p.id+"AddLayerTransparent"), + ), + giu.Row( + giu.Label("Draw effect: "), + giu.Combo("", drawEffectList[state.newLayerFields.DrawEffect], + drawEffectList, &state.newLayerFields.DrawEffect).Size(bigListW).ID( + "##"+p.id+"AddLayerDrawEffect", + ), + ), + giu.Row( + giu.Label("Weapon class: "), + giu.Combo("", weaponClassList[state.newLayerFields.WeaponClass], + weaponClassList, &state.newLayerFields.WeaponClass).Size(bigListW).ID( + "##"+p.id+"AddLayerWeaponClass", + ), + ), + giu.Separator(), + p.makeSaveCancelButtonRow(available, state), + } +} + +func (p *widget) makeSaveCancelButtonRow(available []d2enum.CompositeType, state *widgetState) *giu.RowWidget { + fnSave := func() { + newCofLayer := &d2cof.CofLayer{ + Type: available[state.newLayerFields.LayerType], + Shadow: state.newLayerFields.Shadow, + Selectable: state.newLayerFields.Selectable, + Transparent: state.newLayerFields.Transparent, + DrawEffect: d2enum.DrawEffect(state.newLayerFields.DrawEffect), + WeaponClass: d2enum.WeaponClass(state.newLayerFields.WeaponClass), + } + + p.cof.CofLayers = append(p.cof.CofLayers, *newCofLayer) + + p.cof.NumberOfLayers++ + + for dirIdx := range p.cof.Priority { + for frameIdx := range p.cof.Priority[dirIdx] { + p.cof.Priority[dirIdx][frameIdx] = append(p.cof.Priority[dirIdx][frameIdx], newCofLayer.Type) + } + } + + // this sets layer index to just added layer + state.viewerState.LayerIndex = int32(p.cof.NumberOfLayers - 1) + state.viewerState.layer = newCofLayer + state.Mode = modeViewer + } + + fnCancel := func() { + state.Mode = modeViewer + } + + buttonSave := giu.Button("Save##AddLayer").Size(saveCancelButtonW, saveCancelButtonH).OnClick(fnSave) + buttonCancel := giu.Button("Cancel##AddLayer").Size(saveCancelButtonW, saveCancelButtonH).OnClick(fnCancel) + + return giu.Row(buttonSave, buttonCancel) +} + +func (p *widget) deleteCurrentLayer(index int32) { + p.cof.NumberOfLayers-- + + newPriority := make([][][]d2enum.CompositeType, p.cof.NumberOfDirections) + + for dn := range p.cof.Priority { + newPriority[dn] = make([][]d2enum.CompositeType, p.cof.FramesPerDirection) + for fn := range p.cof.Priority[dn] { + newPriority[dn][fn] = make([]d2enum.CompositeType, p.cof.NumberOfLayers) + + for ln := range p.cof.Priority[dn][fn] { + if p.cof.CofLayers[index].Type != p.cof.Priority[dn][fn][ln] { + newPriority[dn][fn] = append(newPriority[dn][fn], p.cof.Priority[dn][fn][ln]) + } + } + } + } + + p.cof.Priority = newPriority + + newLayers := make([]d2cof.CofLayer, 0) + + for n, i := range p.cof.CofLayers { + if int32(n) != index { + newLayers = append(newLayers, i) + } + } + + p.cof.CofLayers = newLayers + + state := p.getState() + + if state.viewerState.LayerIndex != 0 { + state.viewerState.LayerIndex-- + } +} + +func (p *widget) duplicateDirection() { + state := p.getState() + + idx := state.viewerState.DirectionIndex + + p.cof.NumberOfDirections++ + + p.cof.Priority = append(p.cof.Priority, p.cof.Priority[idx]) + + state.DirectionIndex = int32(len(p.cof.Priority) - 1) +} + +func (p *widget) deleteCurrentDirection() { + state := p.getState() + + index := state.viewerState.DirectionIndex + + p.cof.NumberOfDirections-- + + newPriority := make([][][]d2enum.CompositeType, 0) + + for n, i := range p.cof.Priority { + if int32(n) != index { + newPriority = append(newPriority, i) + } + } + + p.cof.Priority = newPriority +} diff --git a/pkg/widgets/custom_widgets.go b/pkg/widgets/custom_widgets.go new file mode 100644 index 00000000..b1779a9e --- /dev/null +++ b/pkg/widgets/custom_widgets.go @@ -0,0 +1,222 @@ +package widgets + +import ( + "fmt" + "math" + + "github.com/AllenDang/cimgui-go/imgui" + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/assets" + "github.com/gucio321/HellSpawner/pkg/common" +) + +// MakeImageButton is a hack for giu.ImageButton that creates image button +// as a giu.child +func MakeImageButton(id giu.ID, w, h int, t *giu.Texture, fn func()) giu.Widget { + // the image button + btnW, btnH := float32(w), float32(h) + button := giu.Layout{ + giu.ImageButton(t).Size(btnW, btnH).OnClick(fn), + } + + return giu.Layout{ + giu.Custom(func() { + imgui.PushIDStr(string(id)) + }), + button, + giu.Custom(func() { + imgui.PopID() + }), + } +} + +type playPauseButtonState struct { + playTexture, + pauseTexture *giu.Texture +} + +func (s *playPauseButtonState) Dispose() { + s.playTexture = nil + s.pauseTexture = nil +} + +// PlayPauseButtonWidget represents a play/pause button +type PlayPauseButtonWidget struct { + id string + + onChange, + onPauseClicked, + onPlayClicked func() + + width, + height float32 + + isPlaying *bool + textureLoader common.TextureLoader +} + +// PlayPauseButton creates a play/pause button +func PlayPauseButton(id string, isPlaying *bool, tl common.TextureLoader) *PlayPauseButtonWidget { + return &PlayPauseButtonWidget{ + id: id, + textureLoader: tl, + isPlaying: isPlaying, + } +} + +// Size sets button's size +func (p *PlayPauseButtonWidget) Size(w, h float32) *PlayPauseButtonWidget { + p.width, p.height = w, h + return p +} + +// OnPlayClicked sets onPlayClicked callback (called when the user clicks on play button) +func (p *PlayPauseButtonWidget) OnPlayClicked(cb func()) *PlayPauseButtonWidget { + p.onPlayClicked = cb + return p +} + +// OnPauseClicked sets onPauseClicked callback (called when the user clicks on pause button) +func (p *PlayPauseButtonWidget) OnPauseClicked(cb func()) *PlayPauseButtonWidget { + p.onPauseClicked = cb + return p +} + +// OnChange sets onChange callback (called the user click on any button) +func (p *PlayPauseButtonWidget) OnChange(cb func()) *PlayPauseButtonWidget { + p.onChange = cb + return p +} + +// Build build a widget +func (p *PlayPauseButtonWidget) Build() { + stateID := giu.ID(fmt.Sprintf("%s_state", p.id)) + state := giu.Context.GetState(stateID) + + var widget giu.Widget + + if state == nil { + widget = giu.Image(nil).Size(p.width, p.height) + + state := &playPauseButtonState{} + + p.textureLoader.CreateTextureFromFile(assets.PlayButtonIcon, func(t *giu.Texture) { + state.playTexture = t + }) + + p.textureLoader.CreateTextureFromFile(assets.PauseButtonIcon, func(t *giu.Texture) { + state.pauseTexture = t + }) + + giu.Context.SetState(stateID, state) + + widget.Build() + + return + } + + imgState := state.(*playPauseButtonState) + + w, h := int(p.width), int(p.height) + + var id giu.ID + + var texture *giu.Texture + + var callback func() // callback + + setIsPlaying := func(b bool) { + *p.isPlaying = b + + if cb := p.onChange; cb != nil { + cb() + } + + if cb := p.onPlayClicked; cb != nil { + cb() + } + } + + if !*p.isPlaying { + id = giu.ID(p.id + "Play") + texture = imgState.playTexture + callback = func() { setIsPlaying(true) } + } else { + id = giu.ID(p.id + "Pause") + texture = imgState.pauseTexture + callback = func() { setIsPlaying(false) } + } + + widget = MakeImageButton(id, w, h, texture, callback) + + widget.Build() +} + +// SetByteToInt sets byte given to intager +// if intager > max possible byte size, sets to 255 +func SetByteToInt(input int32, output *byte) { + if input > int32(math.MaxUint8) { + *output = math.MaxUint8 + + return + } + + *output = byte(input) +} + +// MakeInputInt creates input intager using POINTER given +// additionally, for byte checks, if value smaller than 255 +func MakeInputInt(width int32, output interface{}, optionalCB func()) *giu.InputIntWidget { + var input int32 + switch o := output.(type) { + case *byte: + input = int32(*o) + case *int: + input = int32(*o) + default: + panic(fmt.Sprintf("MakeInputInt: invalid value type %T given", o)) + } + + return giu.InputInt(&input).Size(float32(width)).OnChange(func() { + switch o := output.(type) { + case *byte: + SetByteToInt(input, o) + case *int: + *o = int(input) + } + + if optionalCB != nil { + optionalCB() + } + }) +} + +// MakeCheckboxFromByte creates a checkbox using a byte as input/output +func MakeCheckboxFromByte(id giu.ID, value *byte) *giu.CheckboxWidget { + v := *value > 0 + + return giu.Checkbox("", &v).ID(id).OnChange(func() { + if v { + *value = 1 + } else { + *value = 0 + } + }) +} + +// OnDoubleClick detects if item is double-clicked +// this can be used as an alternative to OnClick methos of some widgets +// e.g.: +// +// giu.Layout{ +// giu.Button("double click me"), +// OnDoubleClick(func() { fmt.Println("I was double-clicked") }), +// } +func OnDoubleClick(cb func()) giu.Widget { + return giu.Custom(func() { + if giu.IsItemHovered() && giu.IsMouseDoubleClicked(giu.MouseButtonLeft) { + go cb() + } + }) +} diff --git a/pkg/widgets/dc6widget/doc.go b/pkg/widgets/dc6widget/doc.go new file mode 100644 index 00000000..2f3183c3 --- /dev/null +++ b/pkg/widgets/dc6widget/doc.go @@ -0,0 +1,3 @@ +// Package dc6widget provides a giu.Widget implementation for viewing and editing +// the DC6 data structure. +package dc6widget diff --git a/pkg/widgets/dc6widget/state.go b/pkg/widgets/dc6widget/state.go new file mode 100644 index 00000000..7a9223c8 --- /dev/null +++ b/pkg/widgets/dc6widget/state.go @@ -0,0 +1,282 @@ +package dc6widget + +import ( + "fmt" + "image" + "image/color" + "log" + "time" + + "github.com/AllenDang/giu" + gim "github.com/ozankasikci/go-image-merge" + + "github.com/gucio321/HellSpawner/pkg/common/hsutil" +) + +const ( + miliseconds = 1000 + defaultTickTime = 100 +) + +type animationPlayMode byte + +const ( + playModeForward animationPlayMode = iota + playModeBackword + playModePingPong +) + +func (a animationPlayMode) String() string { + s := map[animationPlayMode]string{ + playModeForward: "Forwards", + playModeBackword: "Backwords", + playModePingPong: "Ping-Pong", + } + + k, ok := s[a] + if !ok { + return "Unknown" + } + + return k +} + +type widgetMode int32 + +const ( + dc6WidgetViewer widgetMode = iota + dc6WidgetTiledView +) + +// widgetState represents dc6 viewer's state +type widgetState struct { + viewerState + tiledState + Mode widgetMode + + IsPlaying bool + Repeat bool + TickTime int32 + PlayMode animationPlayMode + + // cache - will not be saved + rgb []*image.RGBA + textures []*giu.Texture + + IsForward bool + ticker *time.Ticker +} + +func (w *widgetState) Dispose() { + w.viewerState.Dispose() + w.Mode = dc6WidgetViewer + w.textures = nil +} + +type viewerState struct { + Controls struct { + Direction int32 + Frame int32 + Scale int32 + } + + lastFrame int32 + lastDirection int32 + framesPerDirection uint32 +} + +// Dispose disposes state +func (s *viewerState) Dispose() { + // noop +} + +type tiledState struct { + Width, + Height int32 + tiled *giu.Texture + Imgw, + Imgh int +} + +func (s *tiledState) Dispose() { + s.Width, s.Height = 0, 0 + s.tiled = nil +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) initState() { + // Prevent multiple invocation to LoadImage. + newState := &widgetState{ + Mode: dc6WidgetViewer, + viewerState: viewerState{ + lastFrame: -1, + lastDirection: -1, + framesPerDirection: p.dc6.FramesPerDirection, + }, + tiledState: tiledState{ + Width: int32(p.dc6.FramesPerDirection), + Height: 1, + }, + + IsPlaying: false, + Repeat: false, + TickTime: defaultTickTime, + PlayMode: playModeForward, + } + + newState.ticker = time.NewTicker(time.Second * time.Duration(newState.TickTime) / miliseconds) + + go p.runPlayer(newState) + + totalFrames := int(p.dc6.Directions * p.dc6.FramesPerDirection) + newState.rgb = make([]*image.RGBA, totalFrames) + + for frameIndex := 0; frameIndex < int(p.dc6.Directions*p.dc6.FramesPerDirection); frameIndex++ { + newState.rgb[frameIndex] = image.NewRGBA(image.Rect(0, 0, int(p.dc6.Frames[frameIndex].Width), int(p.dc6.Frames[frameIndex].Height))) + decodedFrame := p.dc6.DecodeFrame(frameIndex) + + for y := 0; y < int(p.dc6.Frames[frameIndex].Height); y++ { + for x := 0; x < int(p.dc6.Frames[frameIndex].Width); x++ { + idx := x + (y * int(p.dc6.Frames[frameIndex].Width)) + val := decodedFrame[idx] + + alpha := maxAlpha + + if val == 0 { + alpha = 0 + } + + var r, g, b uint8 + + if p.palette != nil { + col := p.palette[val] + r, g, b = col.R(), col.G(), col.B() + } else { + r, g, b = val, val, val + } + + newState.rgb[frameIndex].Set( + x, y, + color.RGBA{ + R: r, + G: g, + B: b, + A: alpha, + }, + ) + } + } + } + + p.setState(newState) + + go func() { + textures := make([]*giu.Texture, totalFrames) + + for frameIndex := 0; frameIndex < totalFrames; frameIndex++ { + frameIndex := frameIndex + p.textureLoader.CreateTextureFromARGB(newState.rgb[frameIndex], func(t *giu.Texture) { + textures[frameIndex] = t + }) + } + + s := p.getState() + s.textures = textures + p.setState(s) + }() +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} + +func (p *widget) runPlayer(state *widgetState) { + for range state.ticker.C { + if !state.IsPlaying { + continue + } + + numFrames := int32(p.dc6.FramesPerDirection - 1) + isLastFrame := state.Controls.Frame == numFrames + + // update play direction + switch state.PlayMode { + case playModeForward: + state.IsForward = true + case playModeBackword: + state.IsForward = false + case playModePingPong: + if isLastFrame || state.Controls.Frame == 0 { + state.IsForward = !state.IsForward + } + } + + // now update the frame number + if state.IsForward { + state.Controls.Frame++ + } else { + state.Controls.Frame-- + } + + state.Controls.Frame = int32(hsutil.Wrap(int(state.Controls.Frame), int(p.dc6.FramesPerDirection))) + + // next, check for stopping/repeat + isStoppingFrame := (state.Controls.Frame == 0) || (state.Controls.Frame == numFrames) + + if isStoppingFrame && !state.Repeat { + state.IsPlaying = false + } + } +} + +func (p *widget) recalculateTiledViewWidth(state *widgetState) { + // the area of our rectangle must be less or equal than FramesPerDirection + state.Width = int32(p.dc6.FramesPerDirection) / state.Height + p.createImage(state) +} + +func (p *widget) recalculateTiledViewHeight(state *widgetState) { + // the area of our rectangle must be less or equal than FramesPerDirection + state.tiledState.Height = int32(p.dc6.FramesPerDirection) / state.Width + p.createImage(state) +} + +func (p *widget) createImage(state *widgetState) { + firstFrame := state.Controls.Direction * int32(p.dc6.FramesPerDirection) + + grids := make([]*gim.Grid, 0) + + for j := int32(0); j < state.Height*state.Width; j++ { + grids = append(grids, &gim.Grid{Image: image.Image(state.rgb[firstFrame+j])}) + } + + newimg, err := gim.New(grids, int(state.Width), int(state.Height)).Merge() + if err != nil { + log.Printf("merging image error: %v", err) + return + } + + p.textureLoader.CreateTextureFromARGB(newimg, func(t *giu.Texture) { + state.tiled = t + }) + + state.Imgw, state.Imgh = newimg.Bounds().Dx(), newimg.Bounds().Dy() +} diff --git a/pkg/widgets/dc6widget/widget.go b/pkg/widgets/dc6widget/widget.go new file mode 100644 index 00000000..579b080c --- /dev/null +++ b/pkg/widgets/dc6widget/widget.go @@ -0,0 +1,223 @@ +package dc6widget + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/AllenDang/cimgui-go/imgui" + "github.com/AllenDang/giu" + "github.com/OpenDiablo2/dialog" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/common/hsutil" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +const ( + comboW = 125 + inputIntW = 30 + playPauseButtonSize = 15 + buttonW, buttonH = 200, 30 +) + +const ( + maxAlpha = uint8(255) +) + +// widget represents dc6viewer's widget +type widget struct { + id string + dc6 *d2dc6.DC6 + textureLoader common.TextureLoader + palette *[256]d2interface.Color +} + +// Create creates new widget +func Create(state []byte, palette *[256]d2interface.Color, textureLoader common.TextureLoader, id string, dc6 *d2dc6.DC6) giu.Widget { + result := &widget{ + id: id, + dc6: dc6, + textureLoader: textureLoader, + palette: palette, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding dc6 widget state: %v", err) + } + + s.ticker.Reset(time.Second * time.Duration(s.TickTime) / miliseconds) + + if s.Mode == dc6WidgetTiledView { + result.createImage(s) + } + + result.setState(s) + } + + return result +} + +// Build builds a widget +func (p *widget) Build() { + state := p.getState() + + switch state.Mode { + case dc6WidgetViewer: + p.makeViewerLayout().Build() + case dc6WidgetTiledView: + p.makeTiledViewLayout(state).Build() + } +} + +func (p *widget) makeViewerLayout() giu.Layout { + viewerState := p.getState() + + imageScale := uint32(viewerState.Controls.Scale) + curFrameIndex := int(viewerState.Controls.Frame) + (int(viewerState.Controls.Direction) * int(p.dc6.FramesPerDirection)) + dirIdx := int(viewerState.Controls.Direction) + + textureIdx := dirIdx*int(p.dc6.FramesPerDirection) + int(viewerState.Controls.Frame) + + if imageScale < 1 { + imageScale = 1 + } + + // TODO: doesn't work on latest giu + /* + err := giu.Context.GetRenderer().SetTextureMagFilter(giu.TextureFilterNearest) + if err != nil { + log.Print(err) + } + */ + + w := float32(p.dc6.Frames[curFrameIndex].Width * imageScale) + h := float32(p.dc6.Frames[curFrameIndex].Height * imageScale) + + var widget *giu.ImageWidget + if viewerState.textures == nil || len(viewerState.textures) <= int(viewerState.Controls.Frame) || + viewerState.textures[curFrameIndex] == nil { + widget = giu.Image(nil).Size(w, h) + } else { + widget = giu.Image(viewerState.textures[textureIdx]).Size(w, h) + } + + return giu.Layout{ + giu.Label(fmt.Sprintf( + "Version: %v\t Flags: %b\t Encoding: %v\t", + p.dc6.Version, + int64(p.dc6.Flags), + p.dc6.Encoding, + )), + giu.Label(fmt.Sprintf("Directions: %v\tFrames per Direction: %v", p.dc6.Directions, p.dc6.FramesPerDirection)), + giu.Custom(func() { + imgui.BeginGroup() + if p.dc6.Directions > 1 { + imgui.SliderInt("Direction", &viewerState.Controls.Direction, 0, int32(p.dc6.Directions-1)) + } + + if p.dc6.FramesPerDirection > 1 { + imgui.SliderInt("Frames", &viewerState.Controls.Frame, 0, int32(p.dc6.FramesPerDirection-1)) + } + + const minScale, maxScale = 1, 8 + + imgui.SliderInt("Scale", &viewerState.Controls.Scale, minScale, maxScale) + + imgui.EndGroup() + }), + giu.Separator(), + p.makePlayerLayout(viewerState), + giu.Separator(), + widget, + giu.Separator(), + giu.Button("Tiled View##"+p.id+"tiledViewButton").Size(buttonW, buttonH).OnClick(func() { + viewerState.Mode = dc6WidgetTiledView + p.createImage(viewerState) + }), + } +} + +func (p *widget) makePlayerLayout(state *widgetState) giu.Layout { + playModeList := make([]string, 0) + for i := playModeForward; i <= playModePingPong; i++ { + playModeList = append(playModeList, i.String()) + } + + pm := int32(state.PlayMode) + + return giu.Layout{ + giu.Row( + giu.Checkbox("Loop##"+p.id+"PlayRepeat", &state.Repeat), + giu.Combo("##"+p.id+"PlayModeList", playModeList[state.PlayMode], playModeList, &pm).OnChange(func() { + state.PlayMode = animationPlayMode(pm) + }).Size(comboW), + giu.InputInt(&state.TickTime).Label("Tick time").Size(inputIntW).OnChange(func() { + state.ticker.Reset(time.Second * time.Duration(state.TickTime) / miliseconds) + }), + widgets.PlayPauseButton("##"+p.id+"PlayPauseAnimation", &state.IsPlaying, p.textureLoader). + Size(playPauseButtonSize, playPauseButtonSize), + giu.Button("Export GIF##"+p.id+"exportGif").OnClick(func() { + err := p.exportGif(state) + if err != nil { + dialog.Message(err.Error()).Error() + } + }), + giu.Button("Export Frames (PNG)##"+p.id+"exportpng").OnClick(func() { + err := p.exportPng(state) + if err != nil { + dialog.Message(err.Error()).Error() + } + }), + ), + } +} + +func (p *widget) makeTiledViewLayout(state *widgetState) giu.Layout { + return giu.Layout{ + giu.Row( + giu.Label("Tiled view:"), + giu.InputInt(&state.Width).Label("Width").Size(inputIntW).OnChange(func() { + p.recalculateTiledViewHeight(state) + }), + giu.InputInt(&state.Height).Label("Height").Size(inputIntW).OnChange(func() { + p.recalculateTiledViewWidth(state) + }), + ), + giu.Image(state.tiled).Size(float32(state.Imgw), float32(state.Imgh)), + giu.Button("Back##"+p.id+"tiledBack").Size(buttonW, buttonH).OnClick(func() { + state.Mode = dc6WidgetViewer + }), + } +} + +func (p *widget) exportGif(state *widgetState) error { + fpd := int32(p.dc6.FramesPerDirection) + firstFrame := state.Controls.Direction * fpd + images := state.rgb[firstFrame : firstFrame+fpd] + + err := hsutil.ExportToGif(images, state.TickTime) + if err != nil { + return fmt.Errorf("error creating gif file: %w", err) + } + + return nil +} + +func (p *widget) exportPng(state *widgetState) error { + images := state.rgb + + err := hsutil.ExportToPng(images) + if err != nil { + return fmt.Errorf("error creating png file: %w", err) + } + + return nil +} diff --git a/pkg/widgets/dccwidget/doc.go b/pkg/widgets/dccwidget/doc.go new file mode 100644 index 00000000..6ff38f0f --- /dev/null +++ b/pkg/widgets/dccwidget/doc.go @@ -0,0 +1,2 @@ +// Package dccwidget contains stuff responsible for viewing and editing the DCC data structure +package dccwidget diff --git a/pkg/widgets/dccwidget/state.go b/pkg/widgets/dccwidget/state.go new file mode 100644 index 00000000..bc037d2b --- /dev/null +++ b/pkg/widgets/dccwidget/state.go @@ -0,0 +1,214 @@ +package dccwidget + +import ( + "fmt" + "image" + "image/color" + "time" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/common/hsutil" +) + +const miliseconds = 1000 + +type animationPlayMode byte + +const ( + playModeForward animationPlayMode = iota + playModeBackword + playModePingPong +) + +func (a animationPlayMode) String() string { + s := map[animationPlayMode]string{ + playModeForward: "Forwards", + playModeBackword: "Backwords", + playModePingPong: "Ping-Pong", + } + + k, ok := s[a] + if !ok { + return "Unknown" + } + + return k +} + +const defaultTickTime = 100 + +type widgetState struct { + Controls struct { + Direction int32 + Frame int32 + Scale int32 + } + + IsPlaying bool + Repeat bool + TickTime int32 + PlayMode animationPlayMode + + // cache - will not be saved + images []*image.RGBA + textures []*giu.Texture + + isForward bool // determines a direction of animation + ticker *time.Ticker +} + +// Dispose cleans viewers state +func (s *widgetState) Dispose() { + s.textures = nil +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) initState() { + // Prevent multiple invocation to LoadImage. + state := &widgetState{ + IsPlaying: false, + Repeat: false, + TickTime: defaultTickTime, + PlayMode: playModeForward, + } + + state.ticker = time.NewTicker(time.Second * time.Duration(state.TickTime) / miliseconds) + + p.setState(state) + + go p.runPlayer(state) + + totalFrames := p.dcc.NumberOfDirections * p.dcc.FramesPerDirection + state.images = make([]*image.RGBA, totalFrames) + + for dirIdx := range p.dcc.Directions { + fw := p.dcc.Directions[dirIdx].Box.Width + fh := p.dcc.Directions[dirIdx].Box.Height + + for frameIdx := range p.dcc.Directions[dirIdx].Frames { + absoluteFrameIdx := (dirIdx * p.dcc.FramesPerDirection) + frameIdx + + frame := p.dcc.Directions[dirIdx].Frames[frameIdx] + pixels := frame.PixelData + + state.images[absoluteFrameIdx] = image.NewRGBA(image.Rect(0, 0, fw, fh)) + + for y := 0; y < fh; y++ { + for x := 0; x < fw; x++ { + idx := x + (y * fw) + if idx >= len(pixels) { + continue + } + + val := pixels[idx] + + RGBAColor := p.makeImagePixel(val) + state.images[absoluteFrameIdx].Set(x, y, RGBAColor) + } + } + } + } + + go func() { + textures := make([]*giu.Texture, totalFrames) + + for frameIndex := 0; frameIndex < totalFrames; frameIndex++ { + frameIndex := frameIndex + p.textureLoader.CreateTextureFromARGB(state.images[frameIndex], func(t *giu.Texture) { + textures[frameIndex] = t + }) + } + + s := p.getState() + s.textures = textures + p.setState(s) + }() +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} + +func (p *widget) makeImagePixel(val byte) color.RGBA { + alpha := maxAlpha + + if val == 0 { + alpha = 0 + } + + var r, g, b uint8 + + if p.palette != nil { + col := p.palette[val] + r, g, b = col.R(), col.G(), col.B() + } else { + r, g, b = val, val, val + } + + RGBAColor := color.RGBA{ + R: r, + G: g, + B: b, + A: alpha, + } + + return RGBAColor +} + +func (p *widget) runPlayer(state *widgetState) { + for range state.ticker.C { + if !state.IsPlaying { + continue + } + + numFrames := int32(p.dcc.FramesPerDirection - 1) + isLastFrame := state.Controls.Frame == numFrames + + // update play direction + switch state.PlayMode { + case playModeForward: + state.isForward = true + case playModeBackword: + state.isForward = false + case playModePingPong: + if isLastFrame || state.Controls.Frame == 0 { + state.isForward = !state.isForward + } + } + + // now update the frame number + if state.isForward { + state.Controls.Frame++ + } else { + state.Controls.Frame-- + } + + state.Controls.Frame = int32(hsutil.Wrap(int(state.Controls.Frame), p.dcc.FramesPerDirection)) + + // next, check for stopping/repeat + isStoppingFrame := (state.Controls.Frame == 0) || (state.Controls.Frame == numFrames) + + if isStoppingFrame && !state.Repeat { + state.IsPlaying = false + } + } +} diff --git a/pkg/widgets/dccwidget/widget.go b/pkg/widgets/dccwidget/widget.go new file mode 100644 index 00000000..43c54dfd --- /dev/null +++ b/pkg/widgets/dccwidget/widget.go @@ -0,0 +1,170 @@ +package dccwidget + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/AllenDang/cimgui-go/imgui" + "github.com/AllenDang/giu" + "github.com/OpenDiablo2/dialog" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/common/hsutil" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +const ( + inputIntW = 30 + playPauseButtonSize = 15 + comboW = 125 +) + +const ( + maxAlpha = uint8(255) +) + +const ( + imageW, imageH = 32, 32 +) + +type widget struct { + id string + dcc *d2dcc.DCC + palette *[256]d2interface.Color + textureLoader common.TextureLoader +} + +// Create creates a new dcc widget +func Create(tl common.TextureLoader, state []byte, palette *[256]d2interface.Color, id string, dcc *d2dcc.DCC) giu.Widget { + result := &widget{ + id: id, + dcc: dcc, + palette: palette, + textureLoader: tl, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding dcc widget state: %v", err) + } + + // update ticker + s.ticker.Reset(time.Second * time.Duration(s.TickTime) / miliseconds) + result.setState(s) + } + + return result +} + +// Build build a widget +func (p *widget) Build() { + viewerState := p.getState() + + imageScale := uint32(viewerState.Controls.Scale) + dirIdx := int(viewerState.Controls.Direction) + frameIdx := viewerState.Controls.Frame + + textureIdx := dirIdx*len(p.dcc.Directions[dirIdx].Frames) + int(frameIdx) + + if imageScale < 1 { + imageScale = 1 + } + + // TODO: this doesn't work on latest giu + /* + err := giu.Context.GetRenderer().SetTextureMagFilter(giu.TextureFilterNearest) + if err != nil { + log.Print(err) + } + */ + + var widget *giu.ImageWidget + if viewerState.textures == nil || len(viewerState.textures) <= int(frameIdx) || viewerState.textures[frameIdx] == nil { + widget = giu.Image(nil).Size(imageW, imageH) + } else { + bw := p.dcc.Directions[dirIdx].Box.Width + bh := p.dcc.Directions[dirIdx].Box.Height + w := float32(uint32(bw) * imageScale) + h := float32(uint32(bh) * imageScale) + widget = giu.Image(viewerState.textures[textureIdx]).Size(w, h) + } + + giu.Layout{ + giu.Row( + giu.Label(fmt.Sprintf("Signature: %v", p.dcc.Signature)), + giu.Label(fmt.Sprintf("Version: %v", p.dcc.Version)), + ), + giu.Row( + giu.Label(fmt.Sprintf("Directions: %v", p.dcc.NumberOfDirections)), + giu.Label(fmt.Sprintf("Frames per Direction: %v", p.dcc.FramesPerDirection)), + ), + giu.Custom(func() { + imgui.BeginGroup() + if p.dcc.NumberOfDirections > 1 { + imgui.SliderInt("Direction", &viewerState.Controls.Direction, 0, int32(p.dcc.NumberOfDirections-1)) + } + + if p.dcc.FramesPerDirection > 1 { + imgui.SliderInt("Frames", &viewerState.Controls.Frame, 0, int32(p.dcc.FramesPerDirection-1)) + } + + const minScale, maxScale = 1, 8 + + imgui.SliderInt("Scale", &viewerState.Controls.Scale, minScale, maxScale) + + imgui.EndGroup() + }), + giu.Separator(), + p.makePlayerLayout(viewerState), + giu.Separator(), + widget, + }.Build() +} + +func (p *widget) makePlayerLayout(state *widgetState) giu.Layout { + playModeList := make([]string, 0) + for i := playModeForward; i <= playModePingPong; i++ { + playModeList = append(playModeList, i.String()) + } + + pm := int32(state.PlayMode) + + return giu.Layout{ + giu.Row( + giu.Checkbox("Loop##"+p.id+"PlayRepeat", &state.Repeat), + giu.Combo("##"+p.id+"PlayModeList", playModeList[state.PlayMode], playModeList, &pm).OnChange(func() { + state.PlayMode = animationPlayMode(pm) + }).Size(comboW), + giu.InputInt(&state.TickTime).Label("Tick time").Size(inputIntW).OnChange(func() { + state.ticker.Reset(time.Second * time.Duration(state.TickTime) / miliseconds) + }), + widgets.PlayPauseButton("##"+p.id+"PlayPauseAnimation", &state.IsPlaying, p.textureLoader). + Size(playPauseButtonSize, playPauseButtonSize), + giu.Button("Export GIF##"+p.id+"exportGif").OnClick(func() { + err := p.exportGif(state) + if err != nil { + dialog.Message(err.Error()).Error() + } + }), + ), + } +} + +func (p *widget) exportGif(state *widgetState) error { + fpd := int32(p.dcc.FramesPerDirection) + firstFrame := state.Controls.Direction * fpd + images := state.images[firstFrame : firstFrame+fpd] + + err := hsutil.ExportToGif(images, state.TickTime) + if err != nil { + return fmt.Errorf("error creating gif file: %w", err) + } + + return nil +} diff --git a/pkg/widgets/doc.go b/pkg/widgets/doc.go new file mode 100644 index 00000000..f5e14f5c --- /dev/null +++ b/pkg/widgets/doc.go @@ -0,0 +1,2 @@ +// Package hswidget contains a generic editor widget implementation, along with with concrete editor implementations. +package widgets diff --git a/pkg/widgets/ds1widget/doc.go b/pkg/widgets/ds1widget/doc.go new file mode 100644 index 00000000..532c40ca --- /dev/null +++ b/pkg/widgets/ds1widget/doc.go @@ -0,0 +1,2 @@ +// Package ds1widget provides a giu.Widget for viewing and editing the DS1 data structure. +package ds1widget diff --git a/pkg/widgets/ds1widget/state.go b/pkg/widgets/ds1widget/state.go new file mode 100644 index 00000000..407dcf12 --- /dev/null +++ b/pkg/widgets/ds1widget/state.go @@ -0,0 +1,107 @@ +package ds1widget + +import ( + "fmt" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/assets" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +type widgetMode int32 + +const ( + widgetModeViewer widgetMode = iota + widgetModeAddFile + widgetModeAddObject + widgetModeAddPath + widgetModeConfirm +) + +type ds1Controls struct { + TileX, TileY int32 + Object int32 + Subgroup int32 + Tile struct { + Floor, Wall, Shadow, Sub int32 + } + noObjectsImageTexture *giu.Texture +} + +// ds1AddObjectState represents state of new Object +type ds1AddObjectState struct { + ObjType int32 + ObjID int32 + ObjX int32 + ObjY int32 + ObjFlags int32 +} + +// Dispose clears state +func (t *ds1AddObjectState) Dispose() { + // noop +} + +// ds1AddPathState contains data about new path +type ds1AddPathState struct { + PathAction int32 + PathX int32 + PathY int32 +} + +// Dispose clears state +func (t *ds1AddPathState) Dispose() { + // noop +} + +// widgetState represents ds1 viewers state +type widgetState struct { + *ds1Controls + Mode widgetMode + confirmDialog *widgets.PopUpConfirmDialog + NewFilePath string + addObjectState ds1AddObjectState + addPathState ds1AddPathState +} + +// Dispose clears viewers state +func (is *widgetState) Dispose() { + is.addObjectState.Dispose() + is.addPathState.Dispose() +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} + +func (p *widget) initState() { + state := &widgetState{ + ds1Controls: &ds1Controls{}, + } + + p.textureLoader.CreateTextureFromFile(assets.ImageShrug, func(t *giu.Texture) { + state.ds1Controls.noObjectsImageTexture = t + }) + + p.setState(state) +} diff --git a/pkg/widgets/ds1widget/tile_records.go b/pkg/widgets/ds1widget/tile_records.go new file mode 100644 index 00000000..cc12f434 --- /dev/null +++ b/pkg/widgets/ds1widget/tile_records.go @@ -0,0 +1,21 @@ +package ds1widget + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" +) + +func (p *widget) addFloor(idx int32) { + p.ds1.InsertFloor(int(idx), &d2ds1.Layer{}) +} + +func (p *widget) deleteFloor(idx int32) { + p.ds1.DeleteFloor(int(idx)) +} + +func (p *widget) addWall(idx int32) { + p.ds1.InsertWall(int(idx), &d2ds1.Layer{}) +} + +func (p *widget) deleteWall(idx int32) { + p.ds1.DeleteWall(int(idx)) +} diff --git a/pkg/widgets/ds1widget/widget.go b/pkg/widgets/ds1widget/widget.go new file mode 100644 index 00000000..ca2f7f55 --- /dev/null +++ b/pkg/widgets/ds1widget/widget.go @@ -0,0 +1,755 @@ +package ds1widget + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2path" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +const ( + layerDeleteButtonSize = 24 + inputIntW = 40 + filePathW = 200 + deleteButtonSize = 15 + actionButtonW, actionButtonH = 170, 30 + saveCancelButtonW, saveCancelButtonH = 80, 30 + bigListW = 200 + imageW, imageH = 32, 32 +) + +type widget struct { + id giu.ID + ds1 *d2ds1.DS1 + deleteButtonTexture *giu.Texture + textureLoader common.TextureLoader +} + +// Create creates a new ds1 viewer +func Create(textureLoader common.TextureLoader, id string, ds1 *d2ds1.DS1, dbt *giu.Texture, state []byte) giu.Widget { + result := &widget{ + id: giu.ID(id), + ds1: ds1, + deleteButtonTexture: dbt, + textureLoader: textureLoader, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding ds1 widget state: %v", err) + } + + result.setState(s) + } + + return result +} + +// Build builds widget - implements giu.Widget +func (p *widget) Build() { + state := p.getState() + + switch state.Mode { + case widgetModeViewer: + p.makeViewerLayout().Build() + case widgetModeAddFile: + p.makeAddFileLayout().Build() + case widgetModeAddObject: + p.makeAddObjectLayout().Build() + case widgetModeAddPath: + p.makeAddPathLayout().Build() + case widgetModeConfirm: + giu.Layout{ + giu.Label("Please confirm your decision"), + state.confirmDialog, + }.Build() + } +} + +// creates standard viewer/editor layout +func (p *widget) makeViewerLayout() giu.Layout { + state := p.getState() + + tabs := []*giu.TabItemWidget{ + giu.TabItem("Files").Layout(p.makeFilesLayout()), + giu.TabItem("Objects").Layout(p.makeObjectsLayout(state)), + giu.TabItem("Tiles").Layout(p.makeTilesTabLayout(state)), + } + + if len(p.ds1.SubstitutionGroups) > 0 { + tabs = append(tabs, giu.TabItem("Substitutions").Layout(p.makeSubstitutionsLayout(state))) + } + + return giu.Layout{ + p.makeDataLayout(), + giu.Separator(), + giu.TabBar().TabItems(tabs...), + } +} + +// makeDataLayout creates basic data layout +// used in p.makeViewerLayout +func (p *widget) makeDataLayout() giu.Layout { + version := int32(p.ds1.Version()) + + state := p.getState() + + w, h := int32(p.ds1.Width()), int32(p.ds1.Height()) + l := giu.Layout{ + giu.Row( + giu.Label("Version: "), + giu.InputInt(&version).Size(inputIntW).OnChange(func() { + state.confirmDialog = widgets.NewPopUpConfirmDialog( + "##"+p.id+"confirmVersionChange", + "Are you sure, you want to change DS1 Version?", + "This value is used while decoding and encoding ds1 file\n"+ + "Please check github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1/ds1_version.go\n"+ + "to get more informations what does version determinates.\n\n"+ + "Continue?", + func() { + p.ds1.SetVersion(int(version)) + state.Mode = widgetModeViewer + }, + func() { + state.Mode = widgetModeViewer + }, + ) + state.Mode = widgetModeConfirm + }), + ), + // giu.Label(fmt.Sprintf("Size: %d x %d tiles", p.ds1.Width, p.ds1.Height)), + giu.Label("Size:"), + giu.Row( + giu.Label("\tWidth: "), + giu.InputInt(&w).Size(inputIntW).OnChange(func() { + state.confirmDialog = widgets.NewPopUpConfirmDialog( + "##"+p.id+"confirmWidthChange", + "Are you really sure, you want to change size of DS1 tiles?", + "This will affect all your tiles in Tile tab.\n"+ + "Continue?", + func() { + p.ds1.SetWidth(int(w)) + state.Mode = widgetModeViewer + }, + func() { + state.Mode = widgetModeViewer + }, + ) + state.Mode = widgetModeConfirm + }), + ), + giu.Row( + giu.Label("\tHeight: "), + giu.InputInt(&h).Size(inputIntW).OnChange(func() { + state.confirmDialog = widgets.NewPopUpConfirmDialog( + "##"+p.id+"confirmWidthChange", + "Are you really sure, you want to change size of DS1 tiles?", + "This will affect all your tiles in Tile tab.\n"+ + "Continue?", + func() { + p.ds1.SetHeight(int(h)) + state.Mode = widgetModeViewer + }, + func() { + state.Mode = widgetModeViewer + }, + ) + state.Mode = widgetModeConfirm + }), + ), + giu.Label(fmt.Sprintf("Substitution Type: %d", p.ds1.SubstitutionType)), + giu.Separator(), + giu.Label("Number of"), + giu.Label(fmt.Sprintf("\tWall Layers: %d", len(p.ds1.Walls))), + giu.Label(fmt.Sprintf("\tFloor Layers: %d", len(p.ds1.Floors))), + giu.Label(fmt.Sprintf("\tShadow Layers: %d", len(p.ds1.Shadows))), + giu.Label(fmt.Sprintf("\tSubstitution Layers: %d", len(p.ds1.Substitutions))), + } + + return l +} + +// makeFilesLayout creates files list +// used in p.makeViewerLayout (files tab) +func (p *widget) makeFilesLayout() giu.Layout { + state := p.getState() + + l := giu.Layout{} + + // iterating using the value should not be a big deal as + // we only expect a handful of strings in this slice. + for n, str := range p.ds1.Files { + currentIdx := n + + l = append(l, giu.Layout{ + giu.Row( + widgets.MakeImageButton( + "##"+p.id+"DeleteFile"+giu.ID(strconv.Itoa(currentIdx)), + deleteButtonSize, deleteButtonSize, + p.deleteButtonTexture, + func() { + p.ds1.Files = append(p.ds1.Files[:currentIdx], p.ds1.Files[currentIdx+1:]...) + }, + ), + giu.Label(str), + ), + }) + } + + return giu.Layout{ + l, + giu.Separator(), + giu.Button("").ID("Add File##"+p.id+"AddFile").Size(actionButtonW, actionButtonH).OnClick(func() { + state.Mode = widgetModeAddFile + }), + } +} + +func (p *widget) makeAddFileLayout() giu.Layout { + state := p.getState() + + return giu.Layout{ + giu.Label("File path:"), + giu.InputText(&state.NewFilePath).Size(filePathW), + giu.Separator(), + giu.Row( + giu.Button("").ID("Add##"+p.id+"addFileAdd").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + p.ds1.Files = append(p.ds1.Files, state.NewFilePath) + state.Mode = widgetModeViewer + }), + giu.Button("").ID("Cancel##"+p.id+"addFileCancel").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + state.Mode = widgetModeViewer + }), + ), + } +} + +// makeObjectsLayout creates Objects info tab +// used in p.makeViewerLayout (in Objects tab) +func (p *widget) makeObjectsLayout(state *widgetState) giu.Layout { + numObjects := int32(len(p.ds1.Objects)) + + l := giu.Layout{} + + if numObjects > 1 { + l = append(l, giu.SliderInt(&state.Object, 0, numObjects-1).Label("Object Index")) + } + + if numObjects > 0 { + l = append(l, p.makeObjectLayout(state)) + } else { + line := giu.Row( + giu.Label("No Objects."), + giu.ImageWithFile("hsassets/images/shrug.png").Size(imageW, imageH), + ) + + l = append(l, line) + } + + l = append( + l, + giu.Separator(), + giu.Row( + giu.Button("").ID("Add new Object...##"+p.id+"AddObject").Size(actionButtonW, actionButtonH).OnClick(func() { + state.Mode = widgetModeAddObject + }), + giu.Button("").ID("Add path to this Object...##"+p.id+"AddPath").Size(actionButtonW, actionButtonH).OnClick(func() { + state.Mode = widgetModeAddPath + }), + widgets.MakeImageButton( + "##"+p.id+"deleteObject", + layerDeleteButtonSize, layerDeleteButtonSize, + p.deleteButtonTexture, + func() { + p.ds1.Objects = append(p.ds1.Objects[:state.Object], p.ds1.Objects[state.Object+1:]...) + }, + ), + ), + ) + + return l +} + +// makeObjectLayout creates informations about single Object +// used in p.makeObjectsLayout +func (p *widget) makeObjectLayout(state *widgetState) giu.Layout { + if objIdx := int(state.Object); objIdx >= len(p.ds1.Objects) { + state.ds1Controls.Object = int32(len(p.ds1.Objects) - 1) + p.setState(state) + } else if objIdx < 0 { + state.ds1Controls.Object = 0 + p.setState(state) + } + + obj := &p.ds1.Objects[int(state.ds1Controls.Object)] + + l := giu.Layout{ + giu.Row( + giu.Label("Type: "), + widgets.MakeInputInt( + inputIntW, + &obj.Type, + nil, + ), + ), + giu.Row( + giu.Label("ID: "), + widgets.MakeInputInt( + inputIntW, + &obj.ID, + nil, + ), + ), + giu.Label("Position (tiles): "), + giu.Row( + giu.Label("\tX: "), + widgets.MakeInputInt( + inputIntW, + &obj.X, + nil, + ), + ), + giu.Row( + giu.Label("\tY: "), + widgets.MakeInputInt( + inputIntW, + &obj.Y, + nil, + ), + ), + giu.Row( + giu.Label("Flags: 0x"), + widgets.MakeInputInt( + inputIntW, + &obj.Flags, + nil, + ), + ), + } + + if len(obj.Paths) > 0 { + const spacerHeight = 16 + + vspace := giu.Dummy(1, spacerHeight) + l = append(l, vspace, p.makePathLayout(state, obj)) + } + + return l +} + +func (p *widget) makeAddObjectLayout() giu.Layout { + state := p.getState() + + return giu.Layout{ + giu.Row( + giu.Label("Type: "), + giu.InputInt(&state.addObjectState.ObjType).Size(inputIntW), + ), + giu.Row( + giu.Label("ID: "), + giu.InputInt(&state.addObjectState.ObjID).Size(inputIntW), + ), + giu.Row( + giu.Label("X: "), + giu.InputInt(&state.addObjectState.ObjX).Size(inputIntW), + ), + giu.Row( + giu.Label("Y: "), + giu.InputInt(&state.addObjectState.ObjY).Size(inputIntW), + ), + giu.Row( + giu.Label("Flags: "), + giu.InputInt(&state.addObjectState.ObjFlags).Size(inputIntW), + ), + giu.Separator(), + giu.Row( + giu.Button("").ID("Save##"+p.id+"AddObjectSave").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + newObject := d2ds1.Object{ + Type: int(state.addObjectState.ObjType), + ID: int(state.addObjectState.ObjID), + X: int(state.addObjectState.ObjX), + Y: int(state.addObjectState.ObjY), + Flags: int(state.addObjectState.ObjFlags), + } + + p.ds1.Objects = append(p.ds1.Objects, newObject) + + state.Mode = widgetModeViewer + }), + giu.Button("").ID("Cancel##"+p.id+"AddObjectCancel").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + state.Mode = widgetModeViewer + }), + ), + } +} + +// makePathLayout creates paths table +// used in p.makeObjectLayout +func (p *widget) makePathLayout(state *widgetState, obj *d2ds1.Object) giu.Layout { + rowWidgets := make([]*giu.TableRowWidget, 0) + + rowWidgets = append(rowWidgets, giu.TableRow( + giu.Label("Index"), + giu.Label("Position"), + giu.Label("Action"), + giu.Label(""), + )) + + for idx := range obj.Paths { + currentIdx := idx + x, y := obj.Paths[idx].Position.X(), obj.Paths[idx].Position.Y() + rowWidgets = append(rowWidgets, giu.TableRow( + giu.Label(fmt.Sprintf("%d", idx)), + giu.Label(fmt.Sprintf("(%d, %d)", int(x), int(y))), + giu.Label(fmt.Sprintf("%d", obj.Paths[idx].Action)), + widgets.MakeImageButton( + "##"+p.id+"deletePath"+giu.ID(strconv.Itoa(currentIdx)), + deleteButtonSize, deleteButtonSize, + p.deleteButtonTexture, + func() { + p.ds1.Objects[state.Object].Paths = append(p.ds1.Objects[state.Object].Paths[:currentIdx], + p.ds1.Objects[state.Object].Paths[currentIdx+1:]...) + }, + ), + )) + } + + return giu.Layout{ + giu.Label("Path Points:"), + giu.Table().FastMode(true).Rows(rowWidgets...), + } +} + +// makeTilesTabLayout creates tiles layout (tile x, y) +func (p *widget) makeTilesTabLayout(state *widgetState) giu.Layout { + l := giu.Layout{} + + tx, ty := int(state.TileX), int(state.TileY) + + if ty < 0 { + state.ds1Controls.TileY = 0 + p.setState(state) + } + + if tx < 0 { + state.ds1Controls.TileX = 0 + p.setState(state) + } + + numRows := p.ds1.Height() + if numRows == 0 { + return l + } + + if ty >= numRows { + state.ds1Controls.TileY = int32(numRows - 1) + p.setState(state) + } + + if numCols := p.ds1.Width(); tx >= numCols { + state.ds1Controls.TileX = int32(numCols - 1) + p.setState(state) + } + + tx, ty = int(state.TileX), int(state.TileY) + + l = append( + l, + giu.SliderInt(&state.ds1Controls.TileX, 0, int32(p.ds1.Width()-1)).Label("Tile X"), + giu.SliderInt(&state.ds1Controls.TileY, 0, int32(p.ds1.Height()-1)).Label("Tile Y"), + giu.TabBar().TabItems( + p.makeTilesGroupLayout(state, tx, ty, d2ds1.FloorLayerGroup), + p.makeTilesGroupLayout(state, tx, ty, d2ds1.WallLayerGroup), + p.makeTilesGroupLayout(state, tx, ty, d2ds1.ShadowLayerGroup), + p.makeTilesGroupLayout(state, tx, ty, d2ds1.SubstitutionLayerGroup), + ), + ) + + return l +} + +// makeTilesGroupLayout creates a tileS group layout +// used in makeTilesTabLayout +func (p *widget) makeTilesGroupLayout(state *widgetState, x, y int, t d2ds1.LayerGroupType) *giu.TabItemWidget { + l := giu.Layout{} + group := p.ds1.GetLayersGroup(t) + numRecords := len(*group) + + // this is a pointer to appropriate record index + var recordIdx *int32 + // addCb is a callback for layer-add button + var addCb func(int32) + // delCb is a callback for layer-delete button + var deleteCb func(int32) + + // sets "everything" ;-) + switch t { + case d2ds1.FloorLayerGroup: + recordIdx = &state.Tile.Floor + addCb = p.addFloor + deleteCb = p.deleteFloor + case d2ds1.WallLayerGroup: + recordIdx = &state.Tile.Wall + addCb = p.addWall + deleteCb = p.deleteWall + case d2ds1.ShadowLayerGroup: + recordIdx = &state.Tile.Shadow + case d2ds1.SubstitutionLayerGroup: + recordIdx = &state.Tile.Sub + } + + var addBtn *giu.ButtonWidget + if addCb != nil { + addBtn = giu.Button("").ID("Add "+giu.ID(t.String())+" ##"+p.id+"addButton"). + Size(actionButtonW, actionButtonH). + OnClick(func() { addCb(*recordIdx) }) + } + + var deleteBtn giu.Widget + if deleteCb != nil { + deleteBtn = widgets.MakeImageButton( + "##"+p.id+"delete"+giu.ID(t.String()), + layerDeleteButtonSize, layerDeleteButtonSize, + p.deleteButtonTexture, + func() { + deleteCb(*recordIdx) + }, + ) + } + + if numRecords > 0 { + // checks, if record index is correct + if int(*recordIdx) >= numRecords { + *recordIdx = int32(numRecords - 1) + + p.setState(state) + } else if *recordIdx < 0 { + *recordIdx = 0 + + p.setState(state) + } + + if numRecords > 1 { + l = append(l, giu.SliderInt(recordIdx, 0, int32(numRecords-1)).Label(t.String())) + } + + l = append(l, p.makeTileLayout((*group)[*recordIdx].Tile(x, y), t)) + } + + return giu.TabItem(t.String()).Layout(giu.Layout{ + l, + giu.Separator(), + giu.Custom(func() { + var l giu.Layout + if btn := addBtn; btn != nil { + l = append(l, btn) + } + if btn := deleteBtn; btn != nil && numRecords > 0 { + l = append(l, btn) + } + giu.Row(l...).Build() + }), + }) +} + +// makeTileLayout creates a single tile's layout +func (p *widget) makeTileLayout(record *d2ds1.Tile, t d2ds1.LayerGroupType) giu.Layout { + // for substitutions, only unknown bytes should be displayed + if t == d2ds1.SubstitutionLayerGroup { + unknown32 := int32(record.Substitution) + + return giu.Layout{ + giu.Row( + giu.Label("Substitute value: "), + giu.InputInt(&unknown32).Size(inputIntW).OnChange(func() { + record.Substitution = uint32(unknown32) + }), + ), + } + } + + // common for shadows/walls/floors (like d2ds1.tileCommonFields) + l := giu.Layout{ + giu.Row( + giu.Label("Prop1: "), + widgets.MakeInputInt( + inputIntW, + &record.Prop1, + nil, + ), + ), + giu.Row( + giu.Label("Sequence: "), + widgets.MakeInputInt( + inputIntW, + &record.Sequence, + nil, + ), + ), + giu.Row( + giu.Label("Unknown1: "), + widgets.MakeInputInt( + inputIntW, + &record.Unknown1, + nil, + ), + ), + giu.Row( + giu.Label("Style: "), + widgets.MakeInputInt( + inputIntW, + &record.Style, + nil, + ), + ), + giu.Row( + giu.Label("Unknown2: "), + widgets.MakeInputInt( + inputIntW, + &record.Unknown2, + nil, + ), + ), + giu.Row( + giu.Label("Hidden: "), + widgets.MakeCheckboxFromByte( + "##"+p.id+"floorHidden", + &record.HiddenBytes, + ), + ), + giu.Row( + giu.Label(fmt.Sprintf("RandomIndex: %v", record.RandomIndex)), + ), + giu.Row( + giu.Label(fmt.Sprintf("YAdjust: %v", record.YAdjust)), + ), + } + + switch t { + case d2ds1.WallLayerGroup: + l = append(l, + giu.Row( + giu.Label("Zero: "), + widgets.MakeInputInt( + inputIntW, + &record.Zero, + nil, + ), + ), + ) + case d2ds1.FloorLayerGroup, d2ds1.ShadowLayerGroup: + l = append(l, + giu.Row( + giu.Label(fmt.Sprintf("Animated: %v", record.Animated)), + ), + ) + } + + return l +} + +func (p *widget) makeSubstitutionsLayout(state *widgetState) giu.Layout { + l := giu.Layout{} + + recordIdx := int(state.Subgroup) + numRecords := len(p.ds1.SubstitutionGroups) + + if p.ds1.SubstitutionGroups == nil || numRecords == 0 { + return l + } + + if recordIdx >= numRecords { + recordIdx = numRecords - 1 + state.Subgroup = int32(recordIdx) + p.setState(state) + } else if recordIdx < 0 { + recordIdx = 0 + state.Subgroup = int32(recordIdx) + p.setState(state) + } + + if numRecords > 1 { + l = append(l, giu.SliderInt(&state.Subgroup, 0, int32(numRecords-1)).Label("Substitution")) + } + + l = append(l, p.makeSubstitutionLayout(&p.ds1.SubstitutionGroups[recordIdx])) + + return l +} + +func (p *widget) makeSubstitutionLayout(group *d2ds1.SubstitutionGroup) giu.Layout { + l := giu.Layout{ + giu.Label(fmt.Sprintf("TileX: %d", group.TileX)), + giu.Label(fmt.Sprintf("TileY: %d", group.TileY)), + giu.Label(fmt.Sprintf("WidthInTiles: %d", group.WidthInTiles)), + giu.Label(fmt.Sprintf("HeightInTiles: %d", group.HeightInTiles)), + giu.Label(fmt.Sprintf("Unknown: 0x%x", group.Unknown)), + } + + return l +} + +func (p *widget) makeAddPathLayout() giu.Layout { + state := p.getState() + + // https://github.com/OpenDiablo2/OpenDiablo2/issues/811 + // this list should be created like in COFWidget.makeAddLayerLayout + actionsList := []string{"1", "2", "3"} + + return giu.Layout{ + giu.Row( + giu.Label("Action: "), + giu.Combo("", + actionsList[state.addPathState.PathAction], + actionsList, &state.addPathState.PathAction, + ).Size(bigListW).ID( + "##"+p.id+"newPathAction", + ), + ), + giu.Label("Vector:"), + giu.Row( + giu.Label("\tX: "), + giu.InputInt(&state.addPathState.PathX).Size(inputIntW), + ), + giu.Row( + giu.Label("\tY: "), + giu.InputInt(&state.addPathState.PathY).Size(inputIntW), + ), + giu.Separator(), + giu.Row( + giu.Button("").ID("Save##"+p.id+"AddPathSave").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + p.addPath() + state.Mode = widgetModeViewer + }), + giu.Button("").ID("Cancel##"+p.id+"AddPathCancel").Size(saveCancelButtonW, saveCancelButtonH).OnClick(func() { + state.Mode = widgetModeViewer + }), + ), + } +} + +func (p *widget) addPath() { + state := p.getState() + + newPath := d2path.Path{ + // npc actions starts from 1 + Action: int(state.addPathState.PathAction) + 1, + Position: d2vector.NewPosition( + float64(state.addPathState.PathX), + float64(state.addPathState.PathY), + ), + } + + p.ds1.Objects[state.Object].Paths = append(p.ds1.Objects[state.Object].Paths, newPath) +} diff --git a/pkg/widgets/dt1widget/doc.go b/pkg/widgets/dt1widget/doc.go new file mode 100644 index 00000000..f8e43dec --- /dev/null +++ b/pkg/widgets/dt1widget/doc.go @@ -0,0 +1,2 @@ +// Package dt1widget contains a giu widget implementation for viewing and editing the dt1 data structure +package dt1widget diff --git a/pkg/widgets/dt1widget/helpers.go b/pkg/widgets/dt1widget/helpers.go new file mode 100644 index 00000000..8cb85ea5 --- /dev/null +++ b/pkg/widgets/dt1widget/helpers.go @@ -0,0 +1,17 @@ +package dt1widget + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" +) + +// we want to render the isometric (floor) and rle (wall) pixel buffers separately +func decodeTileGfxData(blocks []d2dt1.Block, floorPixBuf, wallPixBuf *[]byte, tileYOffset, tileWidth int32) { + for i := range blocks { + switch blocks[i].Format() { + case d2dt1.BlockFormatIsometric: + d2dt1.DecodeTileGfxData([]d2dt1.Block{blocks[i]}, floorPixBuf, tileYOffset, tileWidth) + case d2dt1.BlockFormatRLE: + d2dt1.DecodeTileGfxData([]d2dt1.Block{blocks[i]}, wallPixBuf, tileYOffset, tileWidth) + } + } +} diff --git a/pkg/widgets/dt1widget/state.go b/pkg/widgets/dt1widget/state.go new file mode 100644 index 00000000..6418ef2f --- /dev/null +++ b/pkg/widgets/dt1widget/state.go @@ -0,0 +1,71 @@ +package dt1widget + +import ( + "fmt" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" +) + +type controls struct { + TileGroup int32 + TileVariant int32 + ShowGrid bool + ShowFloor bool + ShowWall bool + SubtileFlag int32 + Scale int32 +} + +// widgetState represents dt1 viewers state +type widgetState struct { + *controls + + LastTileGroup int32 + + tileGroups [][]*d2dt1.Tile + textures [][]map[string]*giu.Texture +} + +// Dispose clears viewers state +func (s *widgetState) Dispose() { + s.textures = nil +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + p.makeTileTextures() + state = p.getState() + } + + return state +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} + +func (p *widget) initState() { + state := &widgetState{ + controls: &controls{ + ShowGrid: true, + ShowFloor: true, + ShowWall: true, + }, + tileGroups: p.groupTilesByIdentity(), + } + + p.setState(state) +} diff --git a/pkg/widgets/dt1widget/sub_tile_flags.go b/pkg/widgets/dt1widget/sub_tile_flags.go new file mode 100644 index 00000000..44094a40 --- /dev/null +++ b/pkg/widgets/dt1widget/sub_tile_flags.go @@ -0,0 +1,71 @@ +package dt1widget + +const ( + subTileFlagBlockWalk = iota + subTileFlagBlockLOS + subTileFlagBlockJump + subTileFlagBlockPlayerWalk + subTileFlagUnknown1 + subTileFlagBlockLight + subTileFlagUnknown2 + subTileFlagUnknown3 +) + +func subTileString(subtile int32) string { + lookup := map[byte]string{ + 1 << 0: "block walk", + 1 << 1: "block light and line of sight", + 1 << 2: "block jump/teleport", + 1 << 3: "block player walk, allow merc walk", + 1 << 4: "unknown #4", + 1 << 5: "block light only", + 1 << 6: "unknown #6", + 1 << 7: "unknown #7", + } + + str, found := lookup[byte(1<> 1 + halfTileH = subtileHeight >> 1 +) + +type tileIdentity string + +func (tileIdentity) fromTile(tile *d2dt1.Tile) tileIdentity { + str := fmt.Sprintf("%d:%d:%d", tile.Type, tile.Style, tile.Sequence) + return tileIdentity(str) +} + +// widget represents dt1 viewers widget +type widget struct { + id giu.ID + dt1 *d2dt1.DT1 + palette *[256]d2interface.Color + textureLoader common.TextureLoader +} + +// Create creates a new dt1 viewers widget +func Create(state []byte, palette *[256]d2interface.Color, textureLoader common.TextureLoader, id string, dt1 *d2dt1.DT1) giu.Widget { + result := &widget{ + id: giu.ID(id), + dt1: dt1, + textureLoader: textureLoader, + palette: palette, + } + + result.registerKeyboardShortcuts() + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding dt1 editor state: %v", err) + } + } + + return result +} + +func (p *widget) registerKeyboardShortcuts() { + // noop +} + +// Build builds a viewer +func (p *widget) Build() { + state := p.getState() + + if state.LastTileGroup != state.controls.TileGroup { + state.LastTileGroup = state.controls.TileGroup + state.controls.TileVariant = 0 + } + + if len(state.tileGroups) == 0 { + giu.Layout{ + giu.Label("Nothing to display"), + }.Build() + + return + } + + tiles := state.tileGroups[int(state.controls.TileGroup)] + tile := tiles[int(state.controls.TileVariant)] + + giu.Layout{ + p.makeTileSelector(), + giu.Separator(), + p.makeTileDisplay(state, tile), + giu.Separator(), + giu.TabBar().TabItems( + giu.TabItem("Info").Layout(p.makeTileInfoTab(tile)), + giu.TabItem("Material").Layout(p.makeMaterialTab(tile)), + giu.TabItem("Subtile Flags").Layout(p.makeSubtileFlags(state, tile)), + ), + }.Build() +} + +func (p *widget) groupTilesByIdentity() [][]*d2dt1.Tile { + result := make([][]*d2dt1.Tile, 0) + + var tileID, groupID tileIdentity + +OUTER: + for tileIdx := range p.dt1.Tiles { + tile := &p.dt1.Tiles[tileIdx] + tileID = tileID.fromTile(tile) + + for groupIdx := range result { + groupID = groupID.fromTile(result[groupIdx][0]) + + if tileID == groupID { + result[groupIdx] = append(result[groupIdx], tile) + continue OUTER + } + } + + result = append(result, []*d2dt1.Tile{tile}) + } + + return result +} + +func (p *widget) makeTileTextures() { + state := p.getState() + textureGroups := make([][]map[string]*giu.Texture, len(state.tileGroups)) + + for groupIdx := range state.tileGroups { + group := make([]map[string]*giu.Texture, len(state.tileGroups[groupIdx])) + + for variantIdx := range state.tileGroups[groupIdx] { + variantIdx := variantIdx + tile := state.tileGroups[groupIdx][variantIdx] + + floorPix, wallPix := p.makePixelBuffer(tile) + if len(floorPix) == 0 || len(wallPix) == 0 { + continue + } + + tw, th := int(tile.Width), int(tile.Height) + if th < 0 { + th *= -1 + } + + rect := image.Rect(0, 0, tw, th) + imgFloor, imgWall := image.NewRGBA(rect), image.NewRGBA(rect) + imgFloor.Pix, imgWall.Pix = floorPix, wallPix + + p.textureLoader.CreateTextureFromARGB(imgFloor, func(tex *giu.Texture) { + if group[variantIdx] == nil { + group[variantIdx] = make(map[string]*giu.Texture) + } + + group[variantIdx]["floor"] = tex + }) + + p.textureLoader.CreateTextureFromARGB(imgWall, func(tex *giu.Texture) { + if group[variantIdx] == nil { + group[variantIdx] = make(map[string]*giu.Texture) + } + + group[variantIdx]["wall"] = tex + }) + } + + textureGroups[groupIdx] = group + } + + state.textures = textureGroups + + p.setState(state) +} + +func (p *widget) makePixelBuffer(tile *d2dt1.Tile) (floorBuf, wallBuf []byte) { + const ( + rOff = iota // rg,b offsets + gOff + bOff + aOff + bpp // bytes per pixel + ) + + tw, th := int(tile.Width), int(tile.Height) + if th < 0 { + th *= -1 + } + + var tileYMinimum int32 + + for _, block := range tile.Blocks { + tileYMinimum = d2math.MinInt32(tileYMinimum, int32(block.Y)) + } + + tileYOffset := d2math.AbsInt32(tileYMinimum) + + floor := make([]byte, tw*th) // indices into palette + wall := make([]byte, tw*th) // indices into palette + + decodeTileGfxData(tile.Blocks, &floor, &wall, tileYOffset, tile.Width) + + floorBuf = make([]byte, tw*th*bpp) + wallBuf = make([]byte, tw*th*bpp) + + for idx := range floor { + var r, g, b, alpha byte + + floorVal := floor[idx] + wallVal := wall[idx] + + rPos, gPos, bPos, aPos := idx*bpp+rOff, idx*bpp+gOff, idx*bpp+bOff, idx*bpp+aOff + + // the faux rgb color data here is just to make it look more interesting + if p.palette != nil { + col := p.palette[floorVal] + r, g, b = col.R(), col.G(), col.B() + } else { + r = floorVal + g = floorVal + b = floorVal + } + + floorBuf[rPos] = r + floorBuf[gPos] = g + floorBuf[bPos] = b + + if floorVal > 0 { + alpha = 255 + } else { + alpha = 0 + } + + floorBuf[aPos] = alpha + + if p.palette != nil { + col := p.palette[wallVal] + r, g, b = col.R(), col.G(), col.B() + } else { + r = wallVal + g = wallVal + b = wallVal + } + + wallBuf[rPos] = r + wallBuf[gPos] = g + wallBuf[bPos] = b + + if wallVal > 0 { + alpha = 255 + } else { + alpha = 0 + } + + wallBuf[aPos] = alpha + } + + return floorBuf, wallBuf +} + +func (p *widget) makeTileSelector() giu.Layout { + state := p.getState() + + if state.LastTileGroup != state.controls.TileGroup { + state.LastTileGroup = state.controls.TileGroup + state.controls.TileVariant = 0 + } + + numGroups := len(state.tileGroups) - 1 + numVariants := len(state.tileGroups[state.controls.TileGroup]) - 1 + + // actual layout + layout := giu.Layout{ + giu.SliderInt(&state.controls.TileGroup, 0, int32(numGroups)).Label("Tile Group"), + } + + if numVariants > 1 { + layout = append(layout, giu.SliderInt(&state.controls.TileVariant, 0, int32(numVariants)).Label("Tile Variant")) + } + + p.setState(state) + + return layout +} + +// nolint:funlen,gocognit,gocyclo // no need to change +func (p *widget) makeTileDisplay(state *widgetState, tile *d2dt1.Tile) *giu.Layout { + layout := giu.Layout{} + + // nolint:gocritic // could be useful + // curFrameIndex := int(state.controls.frame) + (int(state.controls.direction) * int(p.dt1.FramesPerDirection)) + + if uint32(state.controls.Scale) < 1 { + state.controls.Scale = 1 + } + + // TODO: this is disabled in giu since migration + /* + err := giu.Context.GetRenderer().SetTextureMagFilter(giu.TextureFilterNearest) + if err != nil { + log.Println(err) + } + */ + + w, h := float32(tile.Width), float32(tile.Height) + if h < 0 { + h *= -1 + } + + curGroup, curVariant := int(state.controls.TileGroup), int(state.controls.TileVariant) + + var floorTexture, wallTexture *giu.Texture + + if state.textures == nil || + len(state.textures) <= curGroup || + len(state.textures[curGroup]) <= curVariant || + state.textures[curGroup][curVariant] == nil { + // do nothing + } else { + variant := state.textures[curGroup][curVariant] + + floorTexture = variant["floor"] + wallTexture = variant["wall"] + } + + imageControls := giu.Row( + giu.Checkbox("Show Grid", &state.controls.ShowGrid), + giu.Checkbox("Show Floor", &state.controls.ShowFloor), + giu.Checkbox("Show Wall", &state.controls.ShowWall), + ) + + layout = append(layout, giu.Custom(func() { + canvas := giu.GetCanvas() + pos := giu.GetCursorScreenPos() + + gridOffsetY := int(h - gridMaxHeight + (subtileHeight >> 1)) + if tile.Type == 0 { + // fucking weird special case... + gridOffsetY -= subtileHeight + } + + if state.controls.ShowGrid && (state.controls.ShowFloor || state.controls.ShowWall) { + left := image.Point{X: 0 + pos.X, Y: pos.Y + gridOffsetY} + + halfTileW, halfTileH := subtileWidth>>1, subtileHeight>>1 + + // make TL to BR lines + for idx := 0; idx <= gridDivisionsXY; idx++ { + p1 := image.Point{ + X: left.X + (idx * halfTileW), + Y: left.Y - (idx * halfTileH), + } + + p2 := image.Point{ + X: p1.X + (gridDivisionsXY * halfTileW), + Y: p1.Y + (gridDivisionsXY * halfTileH), + } + + c := colornames.Green + + if idx == 0 || idx == gridDivisionsXY { + c = colornames.Yellowgreen + } + + canvas.AddLine(p1, p2, c, 1) + } + + // make TR to BL lines + for idx := 0; idx <= gridDivisionsXY; idx++ { + p1 := image.Point{ + X: left.X + (idx * halfTileW), + Y: left.Y + (idx * halfTileH), + } + + p2 := image.Point{ + X: p1.X + (gridDivisionsXY * halfTileW), + Y: p1.Y - (gridDivisionsXY * halfTileH), + } + + c := colornames.Green + + if idx == 0 || idx == gridDivisionsXY { + c = colornames.Yellowgreen + } + + canvas.AddLine(p1, p2, c, 1) + } + } + + if state.controls.ShowFloor && floorTexture != nil { + floorTL := image.Point{ + X: pos.X, + Y: pos.Y, + } + + floorBR := image.Point{ + X: floorTL.X + int(w), + Y: floorTL.Y + int(h), + } + + canvas.AddImage(floorTexture, floorTL, floorBR) + } + + if state.controls.ShowWall && wallTexture != nil { + wallTL := image.Point{ + X: pos.X, + Y: pos.Y, + } + + wallBR := image.Point{ + X: wallTL.X + int(w), + Y: wallTL.Y + int(h), + } + + canvas.AddImage(wallTexture, wallTL, wallBR) + } + })) + + if state.controls.ShowFloor || state.controls.ShowWall { + layout = append(layout, giu.Dummy(w, h)) + } + + layout = append(layout, imageControls) + + return &layout +} + +func (p *widget) makeTileInfoTab(tile *d2dt1.Tile) giu.Layout { + // we're creating list of tile names + tileTypeList := make([]string, d2enum.TileRightWallWithDoor+1) + for i := d2enum.TileFloor; i <= d2enum.TileRightWallWithDoor; i++ { + tileTypeList[int(i)] = i.String() + } + + // tileTypeIdx is current index on tile types' list + var tileTypeIdx int32 + // if tileTypeIdx is in range of known names (hsenum.GetTileTypeString) + // then this index is set to tile.Type + // else, we're adding Unknown+#tile.Type to list + // and setting tileTypeIdx to this index + if tile.Type <= int32(d2enum.TileRightWallWithDoor) { + tileTypeIdx = tile.Type + } else { + // nolint:makezero // this is OK + tileTypeList = append(tileTypeList, "Unknown (#"+strconv.Itoa(int(tile.Type))+")") + tileTypeIdx = int32(len(tileTypeList) - 1) + } + + tileTypeInfo := giu.Layout{ + giu.Row( + giu.Label("Type: "), + giu.InputInt(&tile.Type).Size(inputIntW), + giu.Combo("", tileTypeList[tileTypeIdx], tileTypeList, &tile.Type).ID( + "##"+p.id+"tileTypeList", + ), + ), + } + + w, h := tile.Width, tile.Height + if h < 0 { + h *= -1 + } + + roofHeight := int32(tile.RoofHeight) + + const ( + vspaceHeight = 4 // px + ) + + spacer := giu.Dummy(1, vspaceHeight) + + return giu.Layout{ + giu.Row( + giu.InputInt(&w).Size(inputIntW).OnChange(func() { + tile.Width = w + }), + giu.Label(" x "), + giu.InputInt(&h).Size(inputIntW).OnChange(func() { + tile.Height = h + }), + giu.Label("pixels"), + ), + spacer, + + giu.Row( + giu.Label("Direction: "), + giu.InputInt(&tile.Direction).Size(inputIntW), + ), + spacer, + + giu.Row( + giu.Label("RoofHeight:"), + giu.InputInt(&roofHeight).Size(inputIntW).OnChange(func() { + tile.RoofHeight = int16(roofHeight) + }), + ), + spacer, + + tileTypeInfo, + drawTileTypeImage(d2enum.TileType(tile.Type)), + giu.Dummy(1, tiletypeimage.ImageH), + + giu.Row( + giu.Label("Style:"), + giu.InputInt(&tile.Style).Size(inputIntW), + ), + spacer, + + giu.Row( + giu.Label("Sequence:"), + giu.InputInt(&tile.Sequence).Size(inputIntW), + ), + spacer, + + giu.Row( + giu.Label("RarityFrameIndex:"), + giu.InputInt(&tile.RarityFrameIndex).Size(inputIntW), + ), + } +} + +func (p *widget) makeMaterialTab(tile *d2dt1.Tile) giu.Layout { + return giu.Layout{ + giu.Label("Material Flags"), + giu.Table().FastMode(true). + Rows(giu.TableRow( + giu.Checkbox("Other", &tile.MaterialFlags.Other), + giu.Checkbox("Water", &tile.MaterialFlags.Water), + ), + giu.TableRow( + giu.Checkbox("WoodObject", &tile.MaterialFlags.WoodObject), + giu.Checkbox("InsideStone", &tile.MaterialFlags.InsideStone), + ), + giu.TableRow( + giu.Checkbox("OutsideStone", &tile.MaterialFlags.OutsideStone), + giu.Checkbox("Dirt", &tile.MaterialFlags.Dirt), + ), + giu.TableRow( + giu.Checkbox("Sand", &tile.MaterialFlags.Sand), + giu.Checkbox("Wood", &tile.MaterialFlags.Wood), + ), + giu.TableRow( + giu.Checkbox("Lava", &tile.MaterialFlags.Lava), + giu.Checkbox("Snow", &tile.MaterialFlags.Snow), + ), + ), + } +} + +// TileGroup returns current tile group +func (p *widget) TileGroup() int32 { + state := p.getState() + return state.TileGroup +} + +// SetTileGroup sets current tile group +func (p *widget) SetTileGroup(tileGroup int32) { + state := p.getState() + if int(tileGroup) > len(state.tileGroups) { + tileGroup = int32(len(state.tileGroups)) + } else if tileGroup < 0 { + tileGroup = 0 + } + + state.TileGroup = tileGroup +} + +func (p *widget) makeSubtileFlags(state *widgetState, tile *d2dt1.Tile) giu.Layout { + subtileFlagList := make([]string, 0) + + const numberSubtileFlagTypes = 8 + for i := int32(0); i < numberSubtileFlagTypes; i++ { + subtileFlagList = append(subtileFlagList, subTileString(i)) + } + + if tile.Height < 0 { + tile.Height *= -1 + } + + const ( + spacerHeight = 4 // px + ) + + return giu.Layout{ + giu.Combo("", subtileFlagList[state.SubtileFlag], subtileFlagList, &state.SubtileFlag).Size(comboW).ID( + "##" + p.id + "SubtileList", + ), + giu.Label("Edit:"), + giu.Custom(func() { + for y := 0; y < gridDivisionsXY; y++ { + layout := giu.Layout{} + for x := 0; x < gridDivisionsXY; x++ { + layout = append(layout, + giu.Checkbox("##"+strconv.Itoa(y*gridDivisionsXY+x), + p.getSubTileFieldToEdit(y+x*gridDivisionsXY), + ), + ) + } + + giu.Row(layout...).Build() + } + }), + giu.Dummy(0, spacerHeight), + giu.Label("Preview:"), + p.makeSubTilePreview(tile, state), + giu.Dummy(gridMaxWidth, gridMaxHeight), + giu.Label("Click to Add/Remove flags"), + } +} + +func (p *widget) makeSubTilePreview(tile *d2dt1.Tile, state *widgetState) giu.Layout { + return giu.Layout{ + giu.Custom(func() { + canvas := giu.GetCanvas() + pos := giu.GetCursorScreenPos() + + left := image.Point{X: 0 + pos.X, Y: (gridMaxHeight >> 1) + pos.Y} + + // make TL to BR lines + for idx := 0; idx <= gridDivisionsXY; idx++ { + p1 := image.Point{ // top-left point + X: left.X + (idx * halfTileW), + Y: left.Y - (idx * halfTileH), + } + + p2 := image.Point{ // bottom-right point + X: p1.X + (gridDivisionsXY * halfTileW), + Y: p1.Y + (gridDivisionsXY * halfTileH), + } + + c := colornames.Green + + if idx == 0 || idx == gridDivisionsXY { + c = colornames.Yellowgreen + } + + for flagOffsetIdx := 0; flagOffsetIdx < gridDivisionsXY; flagOffsetIdx++ { + if idx == gridDivisionsXY { + continue + } + + ox := (flagOffsetIdx + 1) * halfTileW + oy := flagOffsetIdx * halfTileH + + flagPoint := image.Point{X: p1.X + ox, Y: p1.Y + oy} + + col := colornames.Yellow + + subtileIdx := getFlagFromPos(flagOffsetIdx, idx%gridDivisionsXY) + flag := tile.SubTileFlags[subtileIdx].Encode() + + hasFlag := (flag & (1 << state.controls.SubtileFlag)) > 0 + + p.handleSubtileHoverAndClick(subtileIdx, flagPoint, canvas) + + if hasFlag { + const circleRadius = 3 // px + + canvas.AddCircle(flagPoint, circleRadius, col, 1, 1) + } + } + + canvas.AddLine(p1, p2, c, 1) + } + + // make TR to BL lines + for idx := 0; idx <= gridDivisionsXY; idx++ { + p1 := image.Point{ // bottom left point + X: left.X + (idx * halfTileW), + Y: left.Y + (idx * halfTileH), + } + + p2 := image.Point{ // top-right point + X: p1.X + (gridDivisionsXY * halfTileW), + Y: p1.Y - (gridDivisionsXY * halfTileH), + } + + c := colornames.Green + + if idx == 0 || idx == gridDivisionsXY { + c = colornames.Yellowgreen + } + + canvas.AddLine(p1, p2, c, 1) + } + }), + } +} + +func (p *widget) handleSubtileHoverAndClick(subtileIdx int, flagPoint image.Point, canvas *giu.Canvas) { + mousePos := giu.GetMousePos() + delta := mousePos.Sub(flagPoint) + dx, dy := int(math.Abs(float64(delta.X))), int(math.Abs(float64(delta.Y))) + closeEnough := (dx < halfTileH) && (dy < halfTileH) + + // draw a crosshair on the point if hovered + if closeEnough { + highlight := color.RGBA{255, 255, 255, 64} + + p1, p2 := flagPoint.Sub(image.Point{X: -halfTileW}), flagPoint.Sub(image.Point{X: halfTileW}) + canvas.AddLine(p1, p2, highlight, 1) + + p3, p4 := flagPoint.Sub(image.Point{Y: -halfTileH}), flagPoint.Sub(image.Point{Y: halfTileH}) + canvas.AddLine(p3, p4, highlight, 1) + } + + // on mouse release, toggle the flag + if closeEnough && giu.IsMouseReleased(giu.MouseButtonLeft) { + bit := p.getSubTileFieldToEdit(subtileIdx) + *bit = !(*bit) + } +} diff --git a/pkg/widgets/fonttablewidget/doc.go b/pkg/widgets/fonttablewidget/doc.go new file mode 100644 index 00000000..c060bc4a --- /dev/null +++ b/pkg/widgets/fonttablewidget/doc.go @@ -0,0 +1,3 @@ +// Package fonttablewidget contains a giu widget implementation for viewing and editing the +// font table (tbl) data structure. +package fonttablewidget diff --git a/pkg/widgets/fonttablewidget/state.go b/pkg/widgets/fonttablewidget/state.go new file mode 100644 index 00000000..40106575 --- /dev/null +++ b/pkg/widgets/fonttablewidget/state.go @@ -0,0 +1,88 @@ +package fonttablewidget + +import ( + "fmt" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/assets" +) + +type widgetMode int32 + +const ( + modeViewer widgetMode = iota + modeEditRune + modeAddItem +) + +type widgetState struct { + Mode widgetMode + EditRuneState editRuneState + AddItemState addItemState + deleteButtonTexture *giu.Texture +} + +// Dispose cleans state +func (s *widgetState) Dispose() { + s.EditRuneState.Dispose() + s.AddItemState.Dispose() +} + +type editRuneState struct { + EditedRune int32 + RuneBefore rune +} + +// Dispose disposes a rune state +func (e *editRuneState) Dispose() { + e.EditedRune = rune(0) + e.RuneBefore = rune(0) +} + +type addItemState struct { + NewRune, + Width, + Height int32 +} + +func (s *addItemState) Dispose() { + s.NewRune = rune(0) + s.Height = 0 + s.Width = 0 +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) initState() { + state := &widgetState{ + Mode: modeViewer, + } + + p.textureLoader.CreateTextureFromFile(assets.DeleteIcon, func(texture *giu.Texture) { + state.deleteButtonTexture = texture + }) + + p.setState(state) +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} diff --git a/pkg/widgets/fonttablewidget/widget.go b/pkg/widgets/fonttablewidget/widget.go new file mode 100644 index 00000000..c60dc04c --- /dev/null +++ b/pkg/widgets/fonttablewidget/widget.go @@ -0,0 +1,357 @@ +package fonttablewidget + +import ( + "encoding/json" + "fmt" + "log" + "sort" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2font/d2fontglyph" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/widgets" +) + +const ( + inputIntW = 30 + delSize = 20 + addW, addH = 400, 30 + editRuneW, editRuneH = 50, 30 + saveCancelW, saveCancelH = 80, 30 +) + +type widget struct { + fontTable *d2font.Font + id giu.ID + textureLoader common.TextureLoader +} + +// Create creates a new FontTable widget +func Create( + state []byte, + tl common.TextureLoader, + id string, fontTable *d2font.Font, +) giu.Widget { + result := &widget{ + fontTable: fontTable, + id: giu.ID(id), + textureLoader: tl, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding font table editor state: %v", err) + } + } + + return result +} + +// Build builds a widget +func (p *widget) Build() { + state := p.getState() + + switch state.Mode { + case modeViewer: + p.makeTableLayout().Build() + case modeEditRune: + p.makeEditRuneLayout().Build() + case modeAddItem: + p.makeAddItemLayout().Build() + } +} + +func (p *widget) makeTableLayout() giu.Layout { + state := p.getState() + + rows := make([]*giu.TableRowWidget, 0) + + rows = append(rows, giu.TableRow( + giu.Label("Delete"), + giu.Label("Index"), + giu.Label("Character"), + giu.Label("Width (px)"), + giu.Label("Height (px)"), + )) + + // we need to get keys from map[rune]*d2font.fontGlyph + // and then sort them + chars := make([]rune, len(p.fontTable.Glyphs)) + + // reading runes from map + idx := 0 + + for r := range p.fontTable.Glyphs { + chars[idx] = r + idx++ + } + + // sorting runes + sort.Slice(chars, func(i, j int) bool { + return p.fontTable.Glyphs[chars[i]].FrameIndex() < p.fontTable.Glyphs[chars[j]].FrameIndex() + }) + + for _, idx := range chars { + rows = append(rows, p.makeGlyphLayout(idx)) + } + + return giu.Layout{ + giu.Button("").ID("Add new glyph...##"+p.id+"addItem").Size(addW, addH).OnClick(func() { + state.Mode = modeAddItem + }), + giu.Separator(), + giu.Child().Border(false).Layout(giu.Layout{ + giu.Table().FastMode(true).Rows(rows...), + }), + } +} + +func (p *widget) makeGlyphLayout(r rune) *giu.TableRowWidget { + state := p.getState() + + if p.fontTable.Glyphs[r] == nil { + return &giu.TableRowWidget{} + } + + w := p.fontTable.Glyphs[r].Width() + width32 := int32(w) + + h := p.fontTable.Glyphs[r].Height() + height32 := int32(h) + + row := giu.TableRow( + widgets.MakeImageButton("##"+p.id+"deleteFrame"+giu.ID(r), + delSize, delSize, + state.deleteButtonTexture, + func() { p.deleteRow(r) }, + ), + giu.Row( + giu.Label(fmt.Sprintf("%d", p.fontTable.Glyphs[r].FrameIndex())), + giu.ArrowButton(giu.DirectionUp). + ID("##"+p.id+"upItem"+giu.ID(r)).OnClick(func() { + p.itemUp(r) + }), + giu.ArrowButton(giu.DirectionDown). + ID("##"+p.id+"downItem"+giu.ID(r)).OnClick(func() { + p.itemDown(r) + }), + ), + giu.Row( + giu.Button("").ID("edit##"+p.id+"editRune"+giu.ID(r)).Size(editRuneW, editRuneH).OnClick(func() { + state.EditRuneState.RuneBefore = r + state.EditRuneState.EditedRune = r + state.Mode = modeEditRune + }), + giu.Label(string(r)), + ), + giu.InputInt(&width32).Size(inputIntW).OnChange(func() { + h := p.fontTable.Glyphs[r].Height() + p.fontTable.Glyphs[r].SetSize(int(width32), h) + }), + giu.InputInt(&height32).Size(inputIntW).OnChange(func() { + w := p.fontTable.Glyphs[r].Width() + p.fontTable.Glyphs[r].SetSize(w, int(height32)) + }), + ) + + return row +} + +func (p *widget) deleteRow(r rune) { + delete(p.fontTable.Glyphs, r) +} + +func (p *widget) itemUp(r rune) { + // currentFrame is frame index of 'r' + currentFrame := p.fontTable.Glyphs[r].FrameIndex() + + // checks if above current index (r) is another one + for cr, i := range p.fontTable.Glyphs { + if i.FrameIndex() == currentFrame-1 { + // if above current index ('r') is another one, + // this above row gets down + p.fontTable.Glyphs[cr].SetFrameIndex( + p.fontTable.Glyphs[cr].FrameIndex() + 1, + ) + + break + } + } + + // current row's frame count gets up + p.fontTable.Glyphs[r].SetFrameIndex( + p.fontTable.Glyphs[r].FrameIndex() - 1, + ) +} + +// itemDown does the sam as itemUp +func (p *widget) itemDown(r rune) { + currentFrame := p.fontTable.Glyphs[r].FrameIndex() + + for cr, i := range p.fontTable.Glyphs { + if i.FrameIndex() == currentFrame+1 { + p.fontTable.Glyphs[cr].SetFrameIndex( + p.fontTable.Glyphs[cr].FrameIndex() - 1, + ) + + break + } + } + + p.fontTable.Glyphs[r].SetFrameIndex( + p.fontTable.Glyphs[r].FrameIndex() + 1, + ) +} + +func (p *widget) makeEditRuneLayout() giu.Layout { + state := p.getState() + + r := string(state.EditRuneState.EditedRune) + + return giu.Layout{ + giu.Label("Edit rune:"), + giu.Row( + giu.Label("Rune: "), + giu.InputText(&r).Size(inputIntW).OnChange(func() { + if len(r) > 0 { + state.EditRuneState.EditedRune = int32(r[0]) + } + }), + ), + giu.Row( + giu.Label("Int: "), + giu.InputInt(&state.EditRuneState.EditedRune).Size(inputIntW), + ), + giu.Separator(), + giu.Row( + p.makeSaveCancelRow(func() { + p.fontTable.Glyphs[state.EditRuneState.EditedRune] = p.fontTable.Glyphs[state.EditRuneState.RuneBefore] + p.deleteRow(state.EditRuneState.RuneBefore) + + state.Mode = modeViewer + }, state.EditRuneState.EditedRune), + ), + } +} + +// adds new item on the first free position (frame index) +func (p *widget) makeAddItemLayout() giu.Layout { + state := p.getState() + + // first free index determinates a first frame index, when + // we can place our new item + firstFreeIndex := -1 + + // frame indexes, which are already taken + usedIndexes := make([]int, 0) + + for _, i := range p.fontTable.Glyphs { + usedIndexes = append(usedIndexes, i.FrameIndex()) + } + + sort.Ints(usedIndexes) + + for index, used := range usedIndexes { + // simple condition: + // if index != used, it means that for example + // used indexes are [0, 2, 3], so + // index in list is 1, but usedIndexes[1] is 2, so + // frame 1 is free + if index != used { + firstFreeIndex = index + + break + } + } + + // if no free indexes found, then set to next index + if firstFreeIndex == -1 { + firstFreeIndex = len(usedIndexes) + } + + r := string(state.AddItemState.NewRune) + + return giu.Layout{ + giu.Row( + giu.Label(fmt.Sprintf("Frame index: %d", firstFreeIndex)), + ), + giu.Row( + giu.Label("Rune: "), + // if user put here more then one letter, + // second and further letters will be skipped + giu.InputText(&r).Size(inputIntW).OnChange(func() { + if r == "" { + state.AddItemState.NewRune = 0 + + return + } + + state.AddItemState.NewRune = int32(r[0]) + }), + ), + giu.Row( + giu.Label("Int: "), + giu.InputInt(&state.AddItemState.NewRune).Size(inputIntW), + ), + giu.Row( + giu.Label("Width: "), + giu.InputInt(&state.AddItemState.Width).Size(inputIntW), + ), + giu.Row( + giu.Label("Height: "), + giu.InputInt(&state.AddItemState.Height).Size(inputIntW), + ), + giu.Separator(), + giu.Row( + p.makeSaveCancelRow(func() { + p.addItem(firstFreeIndex) + }, state.AddItemState.NewRune), + ), + } +} + +func (p *widget) addItem(idx int) { + state := p.getState() + + newGlyph := d2fontglyph.Create( + idx, + int(state.AddItemState.Width), + int(state.AddItemState.Height), + ) + + p.fontTable.Glyphs[state.AddItemState.NewRune] = newGlyph + state.Mode = modeViewer +} + +// makeSaveCancelRow creates line of action buttons for an editor +// if given rune already exists in glyph's table, save button isn't +// created +func (p *widget) makeSaveCancelRow(saveCB func(), r rune) giu.Layout { + state := p.getState() + + return giu.Layout{ + giu.Custom(func() { + cancel := giu.Button("").ID("Cancel##"+p.id+"addItemCancel").Size(saveCancelW, saveCancelH).OnClick(func() { + state.Mode = modeViewer + }) + + _, exist := p.fontTable.Glyphs[r] + if exist { + cancel.Build() + + return + } + + giu.Row( + giu.Button("").ID("Save##"+p.id+"addItemSave").Size(saveCancelW, saveCancelH).OnClick(func() { + saveCB() + }), + cancel, + ).Build() + }), + } +} diff --git a/pkg/widgets/palettegrideditorwidget/doc.go b/pkg/widgets/palettegrideditorwidget/doc.go new file mode 100644 index 00000000..5a5feff4 --- /dev/null +++ b/pkg/widgets/palettegrideditorwidget/doc.go @@ -0,0 +1,3 @@ +// Package palettegrideditorwidget provides a giu widget implementation of a palette editor, for viewing and editing +// a palette. +package palettegrideditorwidget diff --git a/pkg/widgets/palettegrideditorwidget/helpers.go b/pkg/widgets/palettegrideditorwidget/helpers.go new file mode 100644 index 00000000..5bf30511 --- /dev/null +++ b/pkg/widgets/palettegrideditorwidget/helpers.go @@ -0,0 +1,18 @@ +package palettegrideditorwidget + +func (p *PaletteGridEditorWidget) changeColor(state *widgetState) { + const ( + maxValue = 255 + rOffset = 24 + gOffset = 16 + bOffset = 8 + aOffset = 0 + ) + + var rgba uint32 + rgba |= uint32(state.RGBA.R) << rOffset + rgba |= uint32(state.RGBA.G) << gOffset + rgba |= uint32(state.RGBA.B) << bOffset + rgba |= uint32(maxValue) << aOffset + (*p.colors)[state.Idx].SetRGBA(rgba) +} diff --git a/pkg/widgets/palettegrideditorwidget/state.go b/pkg/widgets/palettegrideditorwidget/state.go new file mode 100644 index 00000000..212acb78 --- /dev/null +++ b/pkg/widgets/palettegrideditorwidget/state.go @@ -0,0 +1,67 @@ +package palettegrideditorwidget + +import ( + "fmt" + "image/color" + + "github.com/AllenDang/giu" +) + +type widgetMode int32 + +const ( + widgetModeGrid widgetMode = iota + widgetModeEdit +) + +// PaletteGridState represents palette grid's state +type widgetState struct { + Mode widgetMode `json:"mode"` + editEntryState +} + +// Dispose cleans palette grids state +func (ws *widgetState) Dispose() { + ws.Mode = widgetModeGrid +} + +type editEntryState struct { + Idx int + RGBA color.RGBA +} + +func (ees *editEntryState) Dispose() { + ees.Idx = 0 +} + +func (p *PaletteGridEditorWidget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *PaletteGridEditorWidget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.setState(&widgetState{}) + p.initState() + state = p.getState() + } + + return state +} + +func (p *PaletteGridEditorWidget) initState() { + state := &widgetState{ + Mode: widgetModeGrid, + } + + p.setState(state) +} + +func (p *PaletteGridEditorWidget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} diff --git a/pkg/widgets/palettegrideditorwidget/widget.go b/pkg/widgets/palettegrideditorwidget/widget.go new file mode 100644 index 00000000..1f3b1dac --- /dev/null +++ b/pkg/widgets/palettegrideditorwidget/widget.go @@ -0,0 +1,112 @@ +package palettegrideditorwidget + +import ( + "encoding/json" + "log" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/common/hsutil" + "github.com/gucio321/HellSpawner/pkg/widgets/palettegridwidget" +) + +const ( + actionButtonW, actionButtonH = 250, 30 +) + +// PaletteGridEditorWidget represents a palette grid editor +type PaletteGridEditorWidget struct { + id string + colors *[]palettegridwidget.PaletteColor + textureLoader common.TextureLoader + onChange func() +} + +// Create creates a new palette grid editor widget +func Create(state []byte, + textureLoader common.TextureLoader, + id string, + colors *[]palettegridwidget.PaletteColor) *PaletteGridEditorWidget { + result := &PaletteGridEditorWidget{ + id: id, + colors: colors, + textureLoader: textureLoader, + onChange: nil, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error loading palette grid editor state: %v", err) + } + + result.setState(s) + } + + return result +} + +// OnChange sets on change callback +// this callback is ran, when editor's slider or field gets change +func (p *PaletteGridEditorWidget) OnChange(onChange func()) *PaletteGridEditorWidget { + p.onChange = onChange + return p +} + +// Build Builds a widget +func (p *PaletteGridEditorWidget) Build() { + state := p.getState() + + colors := make([]palettegridwidget.PaletteColor, len(*p.colors)) + for n := range *(p.colors) { + colors[n] = (*p.colors)[n] + } + + grid := palettegridwidget.Create(p.textureLoader, p.id, &colors).OnClick(func(idx int) { + color := hsutil.Color((*p.colors)[idx].RGBA()) + state.RGBA = color + state.Idx = idx + + state.Mode = widgetModeEdit + }) + + grid.Build() + + if state.Mode == widgetModeEdit { + p.buildEditor(grid) + } +} + +func (p *PaletteGridEditorWidget) buildEditor(grid *palettegridwidget.PaletteGridWidget) { + state := p.getState() + + isOpen := state.Mode == widgetModeEdit + onChange := func() { + p.changeColor(state) + grid.UpdateImage() + + if p.onChange != nil { + p.onChange() + } + } + + giu.Layout{ + giu.PopupModal("Edit color").IsOpen(&isOpen).Layout( + giu.ColorEdit("##edit color", &state.RGBA).Flags(giu.ColorEditFlagsNoAlpha), + giu.Separator(), + giu.Button("OK##"+p.id+"editColorOK").Size(actionButtonW, actionButtonH).OnClick(func() { + onChange() + state.Mode = widgetModeGrid + }), + ), + // handle clicking on "X" button of popup + giu.Custom(func() { + if !isOpen { + onChange() + state.Mode = widgetModeGrid + } + }), + }.Build() +} diff --git a/pkg/widgets/palettegridwidget/doc.go b/pkg/widgets/palettegridwidget/doc.go new file mode 100644 index 00000000..270f3966 --- /dev/null +++ b/pkg/widgets/palettegridwidget/doc.go @@ -0,0 +1,3 @@ +// Package palettegridwidget provides data for viewing +// and editing 256-colors palettes +package palettegridwidget diff --git a/pkg/widgets/palettegridwidget/palettecolor.go b/pkg/widgets/palettegridwidget/palettecolor.go new file mode 100644 index 00000000..72f42eec --- /dev/null +++ b/pkg/widgets/palettegridwidget/palettecolor.go @@ -0,0 +1,7 @@ +package palettegridwidget + +// PaletteColor represents palette color +type PaletteColor interface { + RGBA() uint32 + SetRGBA(uint32) +} diff --git a/pkg/widgets/palettegridwidget/state.go b/pkg/widgets/palettegridwidget/state.go new file mode 100644 index 00000000..aeefa215 --- /dev/null +++ b/pkg/widgets/palettegridwidget/state.go @@ -0,0 +1,85 @@ +package palettegridwidget + +import ( + "fmt" + "image" + "image/color" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/common/hsutil" +) + +// PaletteGridState represents palette grid's state +type widgetState struct { + rgba *giu.Texture +} + +// Dispose cleans palette grids state +func (s *widgetState) Dispose() { + s.rgba = nil +} + +func (p *PaletteGridWidget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("PaletteGridWidget_%s", p.id)) +} + +func (p *PaletteGridWidget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.setState(&widgetState{}) + p.initState() + state = p.getState() + } + + return state +} + +func (p *PaletteGridWidget) initState() { + state := &widgetState{} + p.setState(state) + + p.rebuildImage() +} + +func (p *PaletteGridWidget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} + +func (p *PaletteGridWidget) rebuildImage() { + rgb := image.NewRGBA(image.Rect(0, 0, gridWidth*cellSize, gridHeight*cellSize)) + + for y := 0; y < gridHeight*cellSize; y++ { + if y%cellSize == 0 { + continue + } + + for x := 0; x < gridWidth*cellSize; x++ { + if x%cellSize == 0 { + continue + } + + idx := (x / cellSize) + ((y / cellSize) * gridWidth) + + c := (*p.colors)[idx] + col := hsutil.Color(c.RGBA()) + + // nolint:gomnd // const + rgb.Set(x, y, color.RGBA{ + R: col.R, + G: col.G, + B: col.B, + A: 255, + }) + } + } + + p.textureLoader.CreateTextureFromARGB(rgb, func(texture *giu.Texture) { + p.setState(&widgetState{rgba: texture}) + }) +} diff --git a/pkg/widgets/palettegridwidget/widget.go b/pkg/widgets/palettegridwidget/widget.go new file mode 100644 index 00000000..d68e1452 --- /dev/null +++ b/pkg/widgets/palettegridwidget/widget.go @@ -0,0 +1,87 @@ +package palettegridwidget + +import ( + "image" + + "github.com/AllenDang/giu" + + "github.com/gucio321/HellSpawner/pkg/common" +) + +const ( + gridWidth = 16 + gridHeight = 16 + cellSize = 12 +) + +// PaletteGridWidget represents a palette grid +type PaletteGridWidget struct { + id string + colors *[]PaletteColor + textureLoader common.TextureLoader + onClick func(idx int) +} + +// Create creates a new palette grid widget +func Create(tl common.TextureLoader, id string, colors *[]PaletteColor) *PaletteGridWidget { + result := &PaletteGridWidget{ + id: id, + colors: colors, + textureLoader: tl, + onClick: nil, + } + + return result +} + +// OnClick sets onClick callback +func (p *PaletteGridWidget) OnClick(onClick func(idx int)) *PaletteGridWidget { + p.onClick = onClick + return p +} + +// UpdateImage updates a palette image. +// should be called when palete colors gets changed +func (p *PaletteGridWidget) UpdateImage() { + p.rebuildImage() +} + +// Build build a new widget +func (p *PaletteGridWidget) Build() { + state := p.getState() + + // cache variable for a base position of image + var imgBase image.Point + + giu.Layout{ + // just save base cursor position + giu.Custom(func() { + imgBase = giu.GetCursorScreenPos() + }), + giu.Image(state.rgba). + Size(gridWidth*cellSize, gridHeight*cellSize), + // event detector - detects clicking in a cell + giu.Custom(func() { + mousePos := giu.GetMousePos() + + // x, y - cursor position on an image + x := mousePos.X - imgBase.X + y := mousePos.Y - imgBase.Y + + // cellX, cellY - cell cords + cellX, cellY := x/cellSize, y/cellSize + + // check if cell cords are out of bounds + if cellX < 0 || cellY < 0 || cellX >= gridWidth || cellY >= gridHeight { + return + } + + idx := cellY*gridHeight + cellX + + if giu.IsWindowFocused(giu.FocusedFlags(giu.FocusedFlagsNone)) && giu.IsMouseClicked(giu.MouseButtonLeft) { + p.onClick(idx) + p.rebuildImage() + } + }), + }.Build() +} diff --git a/pkg/widgets/palettemapwidget/doc.go b/pkg/widgets/palettemapwidget/doc.go new file mode 100644 index 00000000..1942bc05 --- /dev/null +++ b/pkg/widgets/palettemapwidget/doc.go @@ -0,0 +1,3 @@ +// Package palettemapwidget provides a giu widget implementation of an editor for the +// PL2 palette transform data structure. +package palettemapwidget diff --git a/pkg/widgets/palettemapwidget/enum.go b/pkg/widgets/palettemapwidget/enum.go new file mode 100644 index 00000000..46e7743c --- /dev/null +++ b/pkg/widgets/palettemapwidget/enum.go @@ -0,0 +1,18 @@ +package palettemapwidget + +const ( + transformLightLevelVariations = iota + transformInvColorVariations + transformSelectedUintShift + transformAlphaBlend + transformAdditiveBlend + transformMultiplicativeBlend + transformHueVariations + transformRedTones + transformGreenTones + transformBlueTones + transformUnknownVariations + transformMaxComponentBlend + transformDarkendColorShift + transformTextColorShifts +) diff --git a/pkg/widgets/palettemapwidget/helpers.go b/pkg/widgets/palettemapwidget/helpers.go new file mode 100644 index 00000000..c8c03320 --- /dev/null +++ b/pkg/widgets/palettemapwidget/helpers.go @@ -0,0 +1,62 @@ +package palettemapwidget + +func getPaletteTransformString() []string { + selections := []string{ + "Light Level Variations", + "InvColor Variations", + "Selected Unit Shift", + "Alpha Blend", + "Additive Blend", + "Multiplicative Blend", + "Hue Variations", + "Red Tones", + "Green Tones", + "Blue Tones", + "Unknown Variations", + "MaxComponent Blend", + "Darkened Color Shift", + "Text Colors", + "Text ColorShifts", + } + + return selections +} + +// cannot use map or literal, because len of transforms isn't the same, so +// for example if state.Slider1 = 30, state.selection = 0 (LightLevelVariation) +// and len p.pl2.InvColorVariations = 16, than +// (if we'd use map) we receive "index out of range" panic +func (p *widget) getPaletteIndices(state *widgetState) (indice *[256]byte) { + switch state.Selection { + case transformLightLevelVariations: + indice = &p.pl2.LightLevelVariations[state.Slider1].Indices + case transformInvColorVariations: + indice = &p.pl2.InvColorVariations[state.Slider1].Indices + case transformSelectedUintShift: + indice = &p.pl2.SelectedUintShift.Indices + case transformAlphaBlend: + indice = &p.pl2.AlphaBlend[state.Slider2][state.Slider1].Indices + case transformAdditiveBlend: + indice = &p.pl2.AdditiveBlend[state.Slider1].Indices + case transformMultiplicativeBlend: + indice = &p.pl2.MultiplicativeBlend[state.Slider1].Indices + case transformHueVariations: + indice = &p.pl2.HueVariations[state.Slider1].Indices + case transformRedTones: + indice = &p.pl2.RedTones.Indices + case transformGreenTones: + indice = &p.pl2.GreenTones.Indices + case transformBlueTones: + indice = &p.pl2.BlueTones.Indices + case transformUnknownVariations: + indice = &p.pl2.UnknownVariations[state.Slider1].Indices + case transformMaxComponentBlend: + indice = &p.pl2.MaxComponentBlend[state.Slider1].Indices + case transformDarkendColorShift: + indice = &p.pl2.DarkendColorShift.Indices + case transformTextColorShifts: + indice = &p.pl2.TextColorShifts[state.Slider1].Indices + } + + return indice +} diff --git a/pkg/widgets/palettemapwidget/palette_transform.go b/pkg/widgets/palettemapwidget/palette_transform.go new file mode 100644 index 00000000..6a5d2747 --- /dev/null +++ b/pkg/widgets/palettemapwidget/palette_transform.go @@ -0,0 +1,156 @@ +package palettemapwidget + +import ( + "fmt" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2pl2" + + "github.com/gucio321/HellSpawner/pkg/widgets/palettegrideditorwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/palettegridwidget" +) + +func (p *widget) makeGrid(key string, colors *[256]palettegridwidget.PaletteColor) { + c := make([]palettegridwidget.PaletteColor, len(colors)) + for n := range colors { + c[n] = colors[n] + } + + state := p.getState() + state.textures[key] = palettegridwidget.Create(p.textureLoader, p.id+key, &c).OnClick(func(idx int) { + state.ID = key + state.Idx = idx + state.Mode = widgetModeEditTransform + }) +} + +func (p *widget) getColors(indices *[256]byte) *[256]palettegridwidget.PaletteColor { + result := &[256]palettegridwidget.PaletteColor{} + + for idx := range indices { + // nolint:gomnd // const + if idx > 255 { + break + } + + result[idx] = palettegridwidget.PaletteColor(&p.pl2.BasePalette.Colors[indices[idx]]) + } + + return result +} + +// single transform (256 palette indices) +// example: selected unit +func (p *widget) transformSingle(key string, transform *[256]byte) giu.Layout { + state := p.getState() + + l := giu.Layout{} + + if tex, found := state.textures[key]; found { + l = append(l, tex) + } else { + p.makeGrid(key, p.getColors(transform)) + } + + return l +} + +// multiple transforms (n * 256 palette indices) +// light level variations, there's 32 +func (p *widget) transformMulti(key string, transforms []d2pl2.PL2PaletteTransform) giu.Layout { + state := p.getState() + + l := giu.Layout{} + + numSelections := int32(len(transforms)) + + if state.Slider1 >= numSelections { + state.Slider1 = numSelections - 1 + p.setState(state) + } + + textureID := fmt.Sprintf("%s_%d", key, state.Slider1) + + l = append(l, giu.SliderInt(&state.Slider1, 0, numSelections-1).Label("##"+key+"_slider")) + + if tex, found := state.textures[textureID]; found { + l = append(l, tex) + } else { + p.makeGrid(textureID, p.getColors(&transforms[state.Slider1].Indices)) + } + + return l +} + +// tranferMultiGroup - groups of multiple transforms (m * n * 256 palette indices) +// example: alpha blend, there's 3 alpha levels (25%, 50%, 75% ?), and each do a blend against all 256 colors +func (p *widget) transformMultiGroup(key string, groups ...[256]d2pl2.PL2PaletteTransform) giu.Layout { + state := p.getState() + + l := giu.Layout{} + + numGroups := int32(len(groups)) + + if state.Slider2 >= numGroups { + state.Slider2 = numGroups - 1 + p.setState(state) + } + + if numGroups > 1 { + sliderKey := fmt.Sprintf("##%s_group", key) + l = append(l, giu.SliderInt(&state.Slider2, 0, numGroups-1).Label(sliderKey)) + } + + groupIdx := state.Slider2 + + numSelections := int32(len(groups[groupIdx]) - 1) + + if state.Slider1 >= numSelections { + state.Slider1 = numSelections - 1 + p.setState(state) + } + + textureID := fmt.Sprintf("%s_%d_%d", key, state.Slider2, state.Slider1) + + l = append(l, giu.SliderInt(&state.Slider1, 0, numSelections).Label("##"+key+"_slider")) + + if tex, found := state.textures[textureID]; found { + l = append(l, tex) + } else { + col := p.getColors(&groups[groupIdx][state.Slider1].Indices) + p.makeGrid(textureID, col) + } + + return l +} + +func (p *widget) textColors(key string, colors []d2pl2.PL2Color24Bits) giu.Layout { + state := p.getState() + + l := giu.Layout{} + + numSelections := int32(len(colors) - 1) + + if state.Slider1 >= numSelections { + state.Slider1 = numSelections - 1 + p.setState(state) + } + + textureID := fmt.Sprintf("%s_%d", key, state.Slider1) + if tex, found := state.textures[textureID]; found { + l = append(l, tex) + } else { + c := make([]palettegridwidget.PaletteColor, len(p.pl2.TextColors)) + + for n := range c { + c[n] = palettegridwidget.PaletteColor(&p.pl2.TextColors[n]) + } + + grid := palettegrideditorwidget.Create(nil, p.textureLoader, p.id+"transform24editColor", &c) + + state.textures[textureID] = grid + } + + return l +} diff --git a/pkg/widgets/palettemapwidget/state.go b/pkg/widgets/palettemapwidget/state.go new file mode 100644 index 00000000..f9128658 --- /dev/null +++ b/pkg/widgets/palettemapwidget/state.go @@ -0,0 +1,70 @@ +package palettemapwidget + +import ( + "fmt" + + "github.com/AllenDang/giu" +) + +type widgetMode int + +const ( + widgetModeView widgetMode = iota + widgetModeEditTransform +) + +type widgetState struct { + Mode widgetMode + Selection int32 + Slider1 int32 + Slider2 int32 + textures map[string]giu.Widget + editTransformState +} + +// Dispose cleans viewer's state +func (p *widgetState) Dispose() { + p.textures = make(map[string]giu.Widget) + p.editTransformState.Dispose() +} + +type editTransformState struct { + ID string + Idx int +} + +func (p *editTransformState) Dispose() { + p.ID = "" +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) initState() { + state := &widgetState{ + Mode: widgetModeView, + textures: make(map[string]giu.Widget), + } + + p.setState(state) +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} diff --git a/pkg/widgets/palettemapwidget/widget.go b/pkg/widgets/palettemapwidget/widget.go new file mode 100644 index 00000000..822e5432 --- /dev/null +++ b/pkg/widgets/palettemapwidget/widget.go @@ -0,0 +1,191 @@ +package palettemapwidget + +import ( + "encoding/json" + "log" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2pl2" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/common/hsutil" + "github.com/gucio321/HellSpawner/pkg/widgets/palettegrideditorwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/palettegridwidget" +) + +const ( + comboW = 280 + layoutW, layoutH = 475, 300 + actionButtonW = layoutW + numColorsInPalette = 256 +) + +type widget struct { + id string + pl2 *d2pl2.PL2 + textureLoader common.TextureLoader +} + +// Create creates a new palette map viewer's widget +func Create(textureLoader common.TextureLoader, id string, pl2 *d2pl2.PL2, state []byte) giu.Widget { + result := &widget{ + id: id, + pl2: pl2, + textureLoader: textureLoader, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding palette map widget state: %v", err) + } + } + + return result +} + +// Build builds a new widget +func (p *widget) Build() { + state := p.getState() + + switch state.Mode { + case widgetModeView: + p.buildViewer(state) + case widgetModeEditTransform: + p.buildEditor(state) + } +} + +func (p *widget) buildViewer(state *widgetState) { + // TODO: this is disabled in giu since cimgui-go migration + /* + err := giu.Context.GetRenderer().SetTextureMagFilter(giu.TextureFilterNearest) + if err != nil { + log.Print(err) + } + */ + + baseColors := make([]palettegridwidget.PaletteColor, numColorsInPalette) + + for n := range baseColors { + baseColors[n] = palettegridwidget.PaletteColor(&p.pl2.BasePalette.Colors[n]) + } + + left := giu.Layout{ + giu.Label("Base Palette"), + palettegrideditorwidget.Create(nil, p.textureLoader, p.id+"basePalette", &baseColors).OnChange(func() { + state.textures = make(map[string]giu.Widget) + }), + } + + selections := getPaletteTransformString() + right := giu.Layout{ + giu.Label("Palette Map"), + giu.Layout{ + giu.Combo("", selections[state.Selection], selections, &state.Selection).Size(comboW), + p.getTransformViewLayout(state.Selection), + }, + } + + w1, h1 := float32(layoutW), float32(layoutH) + w2, h2 := float32(layoutW), float32(layoutH) + + // nolint:gomnd // special case for alpha blend + if state.Selection == 3 { + h2 += 32 + } + + layout := giu.Layout{ + giu.Child().Size(w1, h1).Layout(left), + giu.Child().Size(w2, h2).Layout(right), + } + + layout.Build() +} + +func (p *widget) getTransformViewLayout(transformIdx int32) giu.Layout { + buildLayout := []func() giu.Layout{ + func() giu.Layout { + return p.transformMulti("LightLevelVariations", p.pl2.LightLevelVariations[:]) + }, + func() giu.Layout { + return p.transformMulti("InvColorVariations", p.pl2.InvColorVariations[:]) + }, + func() giu.Layout { + return p.transformSingle("SelectedUintShift", &p.pl2.SelectedUintShift.Indices) + }, + func() giu.Layout { + return p.transformMultiGroup("AlphaBlend", p.pl2.AlphaBlend[:]...) + }, + func() giu.Layout { + return p.transformMulti("AdditiveBlend", p.pl2.AdditiveBlend[:]) + }, + func() giu.Layout { + return p.transformMulti("MultiplicativeBlend", p.pl2.MultiplicativeBlend[:]) + }, + func() giu.Layout { + return p.transformMulti("HueVariations", p.pl2.HueVariations[:]) + }, + func() giu.Layout { + return p.transformSingle("RedTones", &p.pl2.RedTones.Indices) + }, + func() giu.Layout { + return p.transformSingle("GreenTones", &p.pl2.GreenTones.Indices) + }, + func() giu.Layout { + return p.transformSingle("BlueTones", &p.pl2.BlueTones.Indices) + }, + func() giu.Layout { + return p.transformMulti("UnknownVariations", p.pl2.UnknownVariations[:]) + }, + func() giu.Layout { + return p.transformMulti("MaxComponentBlend", p.pl2.MaxComponentBlend[:]) + }, + func() giu.Layout { + return p.transformSingle("DarkendColorShift", &p.pl2.DarkendColorShift.Indices) + }, + func() giu.Layout { + return p.textColors("TextColors", p.pl2.TextColors[:]) + }, + func() giu.Layout { + return p.transformMulti("TextColorShifts", p.pl2.TextColorShifts[:]) + }, + } + + return buildLayout[transformIdx]() +} + +func (p *widget) buildEditor(state *widgetState) { + var grid giu.Widget + + indices := p.getPaletteIndices(state) + + colors := make([]palettegridwidget.PaletteColor, len(p.pl2.BasePalette.Colors)) + + for n := range colors { + colors[n] = palettegridwidget.PaletteColor(&p.pl2.BasePalette.Colors[n]) + } + + grid = palettegridwidget.Create(p.textureLoader, p.id+"transformEdit", &colors).OnClick(func(idx int) { + // this is save, because idx is always less than 256 + indices[state.Idx] = byte(idx) + + // reset textures list + state.textures = make(map[string]giu.Widget) + + state.Mode = widgetModeView + }) + labelColor := hsutil.Color(p.pl2.BasePalette.Colors[indices[state.Idx]].RGBA()) + giu.Layout{ + giu.Style().SetColor(giu.StyleColorText, labelColor).To( + giu.Label("Select color from base palette"), + ), + grid, + giu.Separator(), + // if height > 0, then pushItemHeight + giu.Button("Cancel##"+p.id+"cancelEditorButton").Size(actionButtonW, 0).OnClick(func() { + state.Mode = widgetModeView + }), + }.Build() +} diff --git a/pkg/widgets/popupconfirm.go b/pkg/widgets/popupconfirm.go new file mode 100644 index 00000000..b04e4bbc --- /dev/null +++ b/pkg/widgets/popupconfirm.go @@ -0,0 +1,52 @@ +package widgets + +import ( + "log" + + "github.com/AllenDang/giu" +) + +const ( + yesNoButtonW, yesNoButtonH = 40, 25 +) + +// PopUpConfirmDialog represents a pop up dialog +type PopUpConfirmDialog struct { + header string + message string + id giu.ID + yCB func() + nCB func() +} + +// NewPopUpConfirmDialog creates a new pop up dialog (with yes-no options) +func NewPopUpConfirmDialog(id giu.ID, header, message string, yCB, nCB func()) *PopUpConfirmDialog { + result := &PopUpConfirmDialog{ + header: header, + message: message, + id: id, + yCB: yCB, + nCB: nCB, + } + + return result +} + +// Build builds a pop up dialog +func (p *PopUpConfirmDialog) Build() { + if p.header == "" { + log.Print("Header is empty; please ensure, if you're building appropriate dialog") + } + + open := true + giu.Layout{ + giu.PopupModal(p.header + "##" + string(p.id)).IsOpen(&open).Layout(giu.Layout{ + giu.Label(p.message), + giu.Separator(), + giu.Row( + giu.Button("").ID("YES##"+p.id+"ConfirmDialog").Size(yesNoButtonW, yesNoButtonH).OnClick(p.yCB), + giu.Button("").ID("NO##"+p.id+"confirmDialog").Size(yesNoButtonW, yesNoButtonH).OnClick(p.nCB), + ), + }), + }.Build() +} diff --git a/pkg/widgets/selectpalettewidget/doc.go b/pkg/widgets/selectpalettewidget/doc.go new file mode 100644 index 00000000..b21e61e7 --- /dev/null +++ b/pkg/widgets/selectpalettewidget/doc.go @@ -0,0 +1,3 @@ +// Package selectpalettewidget contains palette select widget +// used in dcc, dc6 and dt1 editors +package selectpalettewidget diff --git a/pkg/widgets/selectpalettewidget/widget.go b/pkg/widgets/selectpalettewidget/widget.go new file mode 100644 index 00000000..40869cba --- /dev/null +++ b/pkg/widgets/selectpalettewidget/widget.go @@ -0,0 +1,128 @@ +package selectpalettewidget + +import ( + "log" + "path/filepath" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dat" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + + "github.com/gucio321/HellSpawner/pkg/common" + "github.com/gucio321/HellSpawner/pkg/common/hsfiletypes" + "github.com/gucio321/HellSpawner/pkg/common/hsproject" + "github.com/gucio321/HellSpawner/pkg/config" + "github.com/gucio321/HellSpawner/pkg/window/hstoolwindow/hsmpqexplorer" + "github.com/gucio321/HellSpawner/pkg/window/hstoolwindow/hsprojectexplorer" +) + +const ( + paletteSelectW, paletteSelectH = 400, 600 + actionButtonW, actionButtonH = 200, 30 +) + +// SelectPaletteWidget represents an pop-up MPQ explorer, when we're +// selectin DAT palette +type SelectPaletteWidget struct { + mpqExplorer *hsmpqexplorer.MPQExplorer + projectExplorer *hsprojectexplorer.ProjectExplorer + id string + saveCB func(colors *[256]d2interface.Color) + closeCB func() +} + +// NewSelectPaletteWidget creates a select palette widget +func NewSelectPaletteWidget( + id string, + project *hsproject.Project, + config *config.Config, + saveCB func(colors *[256]d2interface.Color), + closeCB func(), +) *SelectPaletteWidget { + result := &SelectPaletteWidget{ + id: id, + saveCB: saveCB, + closeCB: closeCB, + } + + callback := func(path *common.PathEntry) { + bytes, bytesErr := path.GetFileBytes() + if bytesErr != nil { + log.Print(bytesErr) + + return + } + + ft, err := hsfiletypes.GetFileTypeFromExtension(filepath.Ext(path.FullPath), &bytes) + if err != nil { + log.Print(err) + + return + } + + if ft == hsfiletypes.FileTypePalette { + // load new palette: + paletteData, err := path.GetFileBytes() + if err != nil { + log.Print(err) + } + + palette, err := d2dat.Load(paletteData) + if err != nil { + log.Print(err) + } + + colors := palette.GetColors() + + saveCB(&colors) + closeCB() + } + } + + mpqExplorer, err := hsmpqexplorer.Create(callback, config, 0, 0) + if err != nil { + log.Print(err) + } + + mpqExplorer.SetProject(project) + + result.mpqExplorer = mpqExplorer + + projectExplorer, err := hsprojectexplorer.Create(nil, callback, 0, 0) + if err != nil { + log.Print(err) + } + + projectExplorer.SetProject(project) + + result.projectExplorer = projectExplorer + + return result +} + +// Build builds a widget +func (p *SelectPaletteWidget) Build() { + // always true (we don't use this feature in this case + isOpen := true + giu.Layout{ + giu.PopupModal("##" + p.id + "popUpSelectPalette").IsOpen(&isOpen).Layout(giu.Layout{ + giu.Child().Size(paletteSelectW, paletteSelectH).Layout(giu.Layout{ + p.projectExplorer.GetProjectTreeNodes(), + giu.Layout(p.mpqExplorer.GetMpqTreeNodes()), + giu.Separator(), + giu.Button("Don't use any palette##"+p.id+"selectPaletteDonotUseAny"). + Size(actionButtonW, actionButtonH). + OnClick(func() { + p.saveCB(nil) + p.closeCB() + }), + giu.Button("Exit##"+p.id+"selectPaletteExit"). + Size(actionButtonW, actionButtonH). + OnClick(func() { + p.closeCB() + }), + }), + }), + }.Build() +} diff --git a/pkg/widgets/stringtablewidget/doc.go b/pkg/widgets/stringtablewidget/doc.go new file mode 100644 index 00000000..3ec1beea --- /dev/null +++ b/pkg/widgets/stringtablewidget/doc.go @@ -0,0 +1,3 @@ +// Package stringtablewidget provides methods for editing and +// viewing TextDictionary (map[string]string) used in string tables +package stringtablewidget diff --git a/pkg/widgets/stringtablewidget/helpers.go b/pkg/widgets/stringtablewidget/helpers.go new file mode 100644 index 00000000..915fe54a --- /dev/null +++ b/pkg/widgets/stringtablewidget/helpers.go @@ -0,0 +1,82 @@ +package stringtablewidget + +import ( + "sort" + "strconv" + "strings" +) + +func (p *widget) formatKey(s *string) { + *s = strings.ReplaceAll(*s, " ", "_") +} + +func (p *widget) updateValueText() { + state := p.getState() + + str, found := p.dict[state.Key] + if found { + state.Value = str + } else { + state.Value = "" + } +} + +func (p *widget) calculateFirstFreeNoName() (firstFreeNoName int) { + state := p.getState() + + ints := make([]int, 0) + + for _, key := range state.keys { + if key[0] == '#' { + idx, err := strconv.Atoi(key[1:]) + if err != nil { + continue + } + + ints = append(ints, idx) + } + } + + sort.Ints(ints) + + for n, i := range ints { + if n != i { + firstFreeNoName = n + break + } + } + + return +} + +func (p *widget) generateTableKeys() (keys []string) { + state := p.getState() + + switch { + case state.NumOnly: + for _, key := range state.keys { + if key[0] == '#' { + keys = append(keys, key) + } else { + // labels are sorted, so no-name (starting from # are on top) + break + } + } + case state.Search != "": + for _, key := range state.keys { + s := strings.ToLower(state.Search) + k := strings.ToLower(key) + v := strings.ToLower(p.dict[key]) + + switch { + case strings.Contains(k, s), + strings.Contains(v, s): + keys = append(keys, key) + } + } + default: + keys = state.keys + } + + return +} diff --git a/pkg/widgets/stringtablewidget/state.go b/pkg/widgets/stringtablewidget/state.go new file mode 100644 index 00000000..1f7a7930 --- /dev/null +++ b/pkg/widgets/stringtablewidget/state.go @@ -0,0 +1,96 @@ +package stringtablewidget + +import ( + "fmt" + "sort" + + "github.com/AllenDang/giu" +) + +type widgetMode int32 + +const ( + widgetModeViewer widgetMode = iota + widgetModeAddEdit +) + +type widgetState struct { + Mode widgetMode + keys []string + NumOnly bool + addEditState + Search string +} + +func (ws *widgetState) Dispose() { + ws.Mode = widgetModeViewer + ws.keys = make([]string, 0) + ws.addEditState.Dispose() + ws.Search = "" +} + +type addEditState struct { + Key string + Value string + // NoName is true, when we're viewing only no-named indexes + NoName bool + + // if we used edit button by table entry, + // we can't edit key value in edit layout + Editable bool +} + +func (aes *addEditState) Dispose() { + aes.Key = "" + aes.Value = "" + aes.NoName = false + aes.Editable = false +} + +func (p *widget) getStateID() giu.ID { + return giu.ID(fmt.Sprintf("widget_%s", p.id)) +} + +func (p *widget) getState() *widgetState { + var state *widgetState + + s := giu.Context.GetState(p.getStateID()) + + if s != nil { + state = s.(*widgetState) + } else { + p.initState() + state = p.getState() + } + + return state +} + +func (p *widget) initState() { + state := &widgetState{} + + p.setState(state) + + p.reloadMapValues() +} + +func (p *widget) reloadMapValues() { + state := p.getState() + + keys := make([]string, len(p.dict)) + + n := 0 + + for key := range p.dict { + keys[n] = key + n++ + } + + sort.Strings(keys) + + state.keys = keys +} + +func (p *widget) setState(s giu.Disposable) { + giu.Context.SetState(p.getStateID(), s) +} diff --git a/pkg/widgets/stringtablewidget/widget.go b/pkg/widgets/stringtablewidget/widget.go new file mode 100644 index 00000000..0d424719 --- /dev/null +++ b/pkg/widgets/stringtablewidget/widget.go @@ -0,0 +1,205 @@ +package stringtablewidget + +import ( + "encoding/json" + "log" + "strconv" + + "github.com/AllenDang/giu" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl" +) + +const ( + deleteW, deleteH = 50, 25 + addEditW, addEditH = 200, 30 + actionButtonW, actionButtonH = 100, 30 +) + +type widget struct { + id string + dict d2tbl.TextDictionary +} + +// Create creates a new string table editor widget +func Create(state []byte, id string, dict d2tbl.TextDictionary) giu.Widget { + result := &widget{ + id: id, + dict: dict, + } + + if giu.Context.GetState(result.getStateID()) == nil && state != nil { + s := result.getState() + if err := json.Unmarshal(state, s); err != nil { + log.Printf("error decoding string table editor state: %v", err) + } + + result.setState(s) + } + + return result +} + +func (p *widget) Build() { + state := p.getState() + + switch state.Mode { + case widgetModeViewer: + p.buildTableLayout() + case widgetModeAddEdit: + p.buildAddEditLayout() + } +} + +func (p *widget) buildTableLayout() { + state := p.getState() + + keys := p.generateTableKeys() + + rows := make([]*giu.TableRowWidget, len(keys)+1) + + columns := []string{"key", "value", "action"} + columnWidgets := make([]giu.Widget, len(columns)) + + for idx := range columns { + columnWidgets[idx] = giu.Label(columns[idx]) + } + + rows[0] = giu.TableRow(columnWidgets...) + + for keyIdx, key := range keys { + // first row is header + rows[keyIdx+1] = p.makeTableRow(key) + } + + giu.Layout{ + giu.Button("Add/Edit record##"+p.id+"addEditRecord"). + Size(addEditW, addEditH).OnClick(func() { + state.Editable = true + state.Mode = widgetModeAddEdit + }), + giu.Separator(), + p.makeSearchSection(), + giu.Separator(), + giu.Custom(func() { + if len(keys) == 0 { + giu.Label("Nothing to display.").Build() + + return + } + giu.Layout{ + giu.Child().Border(false).Layout(giu.Layout{ + giu.Table().FastMode(true).Rows(rows...), + }), + }.Build() + }), + }.Build() +} + +func (p *widget) makeTableRow(key string) *giu.TableRowWidget { + state := p.getState() + + return giu.TableRow( + giu.Label(key), + giu.Label(p.dict[key]), + giu.Row( + giu.Button("delete##"+p.id+"deleteString"+key).Size(deleteW, deleteH).OnClick(func() { + delete(p.dict, key) + p.reloadMapValues() + }), + giu.Button("edit##"+p.id+"editButton"+key).Size(deleteW, deleteH).OnClick(func() { + state.Key = key + state.Editable = false + p.updateValueText() + state.Mode = widgetModeAddEdit + }), + ), + ) +} + +func (p *widget) makeSearchSection() giu.Layout { + state := p.getState() + + return giu.Layout{ + giu.Checkbox("only no-named (starting from #) labels##"+p.id+"numOnly", &state.NumOnly), + giu.Custom(func() { + if !state.NumOnly { + giu.Row( + giu.Label("Search:"), + giu.InputText(&state.Search), + ).Build() + } + }), + } +} + +func (p *widget) buildAddEditLayout() { + state := p.getState() + + giu.Layout{ + giu.Label("Key:"), + giu.Custom(func() { + checkbox := giu.Checkbox("no-name##"+p.id+"addEditNoName", &state.NoName).OnChange(func() { + if state.NoName { + firstFreeNoName := p.calculateFirstFreeNoName() + state.Key = "#" + strconv.Itoa(firstFreeNoName) + p.updateValueText() + } + }) + + if state.Editable { + giu.Row( + p.makeKeyField(), + checkbox, + ).Build() + } else { + giu.Label(state.Key).Build() + } + }), + giu.Label("Value:"), + giu.InputTextMultiline(&state.Value), + giu.Separator(), + giu.Row( + giu.Custom(func() { + var btnStr string + + key := state.Key + if key == "" { + return + } + + _, found := p.dict[key] + if found { + btnStr = "Edit" + } else { + btnStr = "Add" + } + + giu.Button(btnStr+"##"+p.id+"addEditAcceptButton"). + Size(actionButtonW, actionButtonH). + OnClick(func() { + p.dict[key] = state.Value + p.reloadMapValues() + state.Mode = widgetModeViewer + }). + Build() + }), + giu.Button("cancel##"+p.id+"addEditCancel"). + Size(actionButtonW, actionButtonH).OnClick(func() { + state.Mode = widgetModeViewer + }), + ), + giu.Separator(), + giu.Label("Tip: enter existing key in key field to edit it"), + giu.Label("Tip: you don't have to enter key; you can just select \"no-name\""), + }.Build() +} + +func (p *widget) makeKeyField() giu.Widget { + state := p.getState() + + return giu.InputText(&state.Key).OnChange(func() { + p.formatKey(&state.Key) + p.updateValueText() + }) +} diff --git a/pkg/window/hseditor/hsanimdataeditor/animation_data_editor.go b/pkg/window/hseditor/hsanimdataeditor/animation_data_editor.go index c3f31369..80715f5f 100644 --- a/pkg/window/hseditor/hsanimdataeditor/animation_data_editor.go +++ b/pkg/window/hseditor/hsanimdataeditor/animation_data_editor.go @@ -13,7 +13,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/animdatawidget" + "github.com/gucio321/HellSpawner/pkg/widgets/animdatawidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hscofeditor/cof_editor.go b/pkg/window/hseditor/hscofeditor/cof_editor.go index cded25a0..769b9382 100644 --- a/pkg/window/hseditor/hscofeditor/cof_editor.go +++ b/pkg/window/hseditor/hscofeditor/cof_editor.go @@ -15,7 +15,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/config" "github.com/gucio321/HellSpawner/pkg/window/hseditor" - "github.com/gucio321/HellSpawner/hswidget/cofwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/cofwidget" ) // static check, to ensure, if cof editor implemented editoWindow diff --git a/pkg/window/hseditor/hsdc6editor/dc6_editor.go b/pkg/window/hseditor/hsdc6editor/dc6_editor.go index da447d69..da976e45 100644 --- a/pkg/window/hseditor/hsdc6editor/dc6_editor.go +++ b/pkg/window/hseditor/hsdc6editor/dc6_editor.go @@ -13,8 +13,8 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/dc6widget" - "github.com/gucio321/HellSpawner/hswidget/selectpalettewidget" + "github.com/gucio321/HellSpawner/pkg/widgets/dc6widget" + "github.com/gucio321/HellSpawner/pkg/widgets/selectpalettewidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hsdcceditor/dcc_editor.go b/pkg/window/hseditor/hsdcceditor/dcc_editor.go index 2bd804ff..82d5e722 100644 --- a/pkg/window/hseditor/hsdcceditor/dcc_editor.go +++ b/pkg/window/hseditor/hsdcceditor/dcc_editor.go @@ -14,8 +14,8 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/dccwidget" - "github.com/gucio321/HellSpawner/hswidget/selectpalettewidget" + "github.com/gucio321/HellSpawner/pkg/widgets/dccwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/selectpalettewidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hsds1editor/ds1_editor.go b/pkg/window/hseditor/hsds1editor/ds1_editor.go index 0d5c9e01..77d3d53f 100644 --- a/pkg/window/hseditor/hsds1editor/ds1_editor.go +++ b/pkg/window/hseditor/hsds1editor/ds1_editor.go @@ -14,7 +14,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/assets" "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/ds1widget" + "github.com/gucio321/HellSpawner/pkg/widgets/ds1widget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hsdt1editor/dt1_editor.go b/pkg/window/hseditor/hsdt1editor/dt1_editor.go index 7a40fcb0..7c37f7f9 100644 --- a/pkg/window/hseditor/hsdt1editor/dt1_editor.go +++ b/pkg/window/hseditor/hsdt1editor/dt1_editor.go @@ -14,8 +14,8 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/dt1widget" - "github.com/gucio321/HellSpawner/hswidget/selectpalettewidget" + "github.com/gucio321/HellSpawner/pkg/widgets/dt1widget" + "github.com/gucio321/HellSpawner/pkg/widgets/selectpalettewidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hsfonttableeditor/font_table_editor.go b/pkg/window/hseditor/hsfonttableeditor/font_table_editor.go index 7abf92e4..d4fbbae8 100644 --- a/pkg/window/hseditor/hsfonttableeditor/font_table_editor.go +++ b/pkg/window/hseditor/hsfonttableeditor/font_table_editor.go @@ -13,7 +13,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/fonttablewidget" + "github.com/gucio321/HellSpawner/pkg/widgets/fonttablewidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hspaletteeditor/palette_editor.go b/pkg/window/hseditor/hspaletteeditor/palette_editor.go index 632c8917..23d601be 100644 --- a/pkg/window/hseditor/hspaletteeditor/palette_editor.go +++ b/pkg/window/hseditor/hspaletteeditor/palette_editor.go @@ -14,8 +14,8 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/palettegrideditorwidget" - "github.com/gucio321/HellSpawner/hswidget/palettegridwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/palettegrideditorwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/palettegridwidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hspalettemapeditor/palette_map_editor.go b/pkg/window/hseditor/hspalettemapeditor/palette_map_editor.go index 23a58740..6e11a468 100644 --- a/pkg/window/hseditor/hspalettemapeditor/palette_map_editor.go +++ b/pkg/window/hseditor/hspalettemapeditor/palette_map_editor.go @@ -13,7 +13,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/palettemapwidget" + "github.com/gucio321/HellSpawner/pkg/widgets/palettemapwidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hseditor/hssoundeditor/soundeditor.go b/pkg/window/hseditor/hssoundeditor/soundeditor.go index 387d9479..d6d2532f 100644 --- a/pkg/window/hseditor/hssoundeditor/soundeditor.go +++ b/pkg/window/hseditor/hssoundeditor/soundeditor.go @@ -18,7 +18,7 @@ import ( "github.com/faiface/beep/wav" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widgets" "github.com/gucio321/HellSpawner/pkg/window/hseditor" g "github.com/AllenDang/giu" @@ -93,7 +93,7 @@ func (s *SoundEditor) Build() { Size(mainWindowW, mainWindowH). Layout(g.Layout{ g.Row( - hswidget.PlayPauseButton("##"+s.Path.GetUniqueID()+"playPause", &isPlaying, s.textureLoader). + widgets.PlayPauseButton("##"+s.Path.GetUniqueID()+"playPause", &isPlaying, s.textureLoader). OnPlayClicked(s.play).OnPauseClicked(s.stop).Size(btnSize, btnSize), g.ProgressBar(progress).Size(-1, progressBarHeight). Overlay(fmt.Sprintf("%d:%02d / %d:%02d", diff --git a/pkg/window/hseditor/hsstringtableeditor/string_table_editor.go b/pkg/window/hseditor/hsstringtableeditor/string_table_editor.go index cbe8ceac..8866566c 100644 --- a/pkg/window/hseditor/hsstringtableeditor/string_table_editor.go +++ b/pkg/window/hseditor/hsstringtableeditor/string_table_editor.go @@ -13,7 +13,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common" "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget/stringtablewidget" + "github.com/gucio321/HellSpawner/pkg/widgets/stringtablewidget" "github.com/gucio321/HellSpawner/pkg/window/hseditor" ) diff --git a/pkg/window/hstoolwindow/hsmpqexplorer/mpqexplorer.go b/pkg/window/hstoolwindow/hsmpqexplorer/mpqexplorer.go index 86f95085..bbff8b80 100644 --- a/pkg/window/hstoolwindow/hsmpqexplorer/mpqexplorer.go +++ b/pkg/window/hstoolwindow/hsmpqexplorer/mpqexplorer.go @@ -19,7 +19,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common/hsstate" "github.com/gucio321/HellSpawner/pkg/common/hsutil" "github.com/gucio321/HellSpawner/pkg/config" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widgets" "github.com/gucio321/HellSpawner/pkg/window/hstoolwindow" ) @@ -146,7 +146,7 @@ func (m *MPQExplorer) renderNodes(pathEntry *common.PathEntry) g.Widget { return g.Layout{ g.Selectable(pathEntry.Name + id), - hswidget.OnDoubleClick(func() { m.fileSelectedCallback(pathEntry) }), + widgets.OnDoubleClick(func() { m.fileSelectedCallback(pathEntry) }), g.ContextMenu().Layout(g.Layout{ g.Selectable("Copy to Project").OnClick(func() { m.copyToProject(pathEntry) diff --git a/pkg/window/hstoolwindow/hsprojectexplorer/projectexplorer.go b/pkg/window/hstoolwindow/hsprojectexplorer/projectexplorer.go index c3122a52..df5f6c56 100644 --- a/pkg/window/hstoolwindow/hsprojectexplorer/projectexplorer.go +++ b/pkg/window/hstoolwindow/hsprojectexplorer/projectexplorer.go @@ -21,7 +21,7 @@ import ( "github.com/gucio321/HellSpawner/pkg/common/hsproject" "github.com/gucio321/HellSpawner/pkg/common/hsstate" "github.com/gucio321/HellSpawner/pkg/common/hsutil" - "github.com/gucio321/HellSpawner/hswidget" + "github.com/gucio321/HellSpawner/pkg/widgets" "github.com/gucio321/HellSpawner/pkg/window/hstoolwindow" ) @@ -213,7 +213,7 @@ func (m *ProjectExplorer) createFileTreeItem(pathEntry *common.PathEntry) g.Widg } else { layout = append(layout, g.Selectable(pathEntry.Name+id), - hswidget.OnDoubleClick(func() { m.fileSelectedCallback(pathEntry) }), + widgets.OnDoubleClick(func() { m.fileSelectedCallback(pathEntry) }), ) }