From 390f6a1234ada8a52be1803612c0db24294e986a Mon Sep 17 00:00:00 2001 From: David Carrell Date: Wed, 24 Jun 2020 17:46:03 -0500 Subject: [PATCH] 351 - add progress handle and helper functions to ScreenLoadHandler:OnLoad to provide ability to asynchronously load data and animate the loading screen (#445) Co-authored-by: carrelda@Davids-MacBook-Pro.local --- d2core/d2gui/d2gui.go | 1 + d2core/d2screen/d2screen.go | 74 ++++++++++++++++++----- d2game/d2gamescreen/blizzard_intro.go | 10 ++- d2game/d2gamescreen/character_select.go | 8 ++- d2game/d2gamescreen/credits.go | 15 +++-- d2game/d2gamescreen/game.go | 6 +- d2game/d2gamescreen/gui_testing.go | 8 ++- d2game/d2gamescreen/main_menu.go | 11 +++- d2game/d2gamescreen/map_engine_testing.go | 7 ++- d2game/d2gamescreen/select_hero_class.go | 11 +++- d2game/d2player/escape_menu.go | 3 - 11 files changed, 117 insertions(+), 37 deletions(-) diff --git a/d2core/d2gui/d2gui.go b/d2core/d2gui/d2gui.go index 5debb715..ef4328dc 100644 --- a/d2core/d2gui/d2gui.go +++ b/d2core/d2gui/d2gui.go @@ -44,6 +44,7 @@ func SetLayout(layout *Layout) { singleton.SetLayout(layout) } +// ShowLoadScreen renders the loading progress screen. The provided progress argument defines the loading animation's state in the range `[0, 1]`, where `0` is initial frame and `1` is the final frame func ShowLoadScreen(progress float64) { verifyWasInit() singleton.showLoadScreen(progress) diff --git a/d2core/d2screen/d2screen.go b/d2core/d2screen/d2screen.go index aa5f315d..04eeb0c0 100644 --- a/d2core/d2screen/d2screen.go +++ b/d2core/d2screen/d2screen.go @@ -1,6 +1,8 @@ package d2screen import ( + "log" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" @@ -9,7 +11,10 @@ import ( type Screen interface{} type ScreenLoadHandler interface { - OnLoad() error + // OnLoad performs all necessary loading to prepare a screen to be shown such as loading assets, placing and binding + // of ui elements, etc. This loading is done asynchronously. The provided channel will allow implementations to + // provide progress via Error, Progress, or Done + OnLoad(loading LoadingState) } type ScreenUnloadHandler interface { @@ -27,6 +32,7 @@ type ScreenAdvanceHandler interface { var singleton struct { nextScreen Screen loadingScreen Screen + loadingState LoadingState currentScreen Screen } @@ -35,7 +41,25 @@ func SetNextScreen(screen Screen) { } func Advance(elapsed float64) error { - if singleton.nextScreen != nil { + switch { + case singleton.loadingScreen != nil: + // this call blocks execution and could lead to deadlock if a screen implements OnLoad incorreclty + load, ok := <-singleton.loadingState.updates + if !ok { + log.Println("loadingState chan should not be closed while in a loading screen") + } + if load.err != nil { + log.Printf("PROBLEM LOADING THE SCREEN: %v", load.err) + return load.err + } + d2gui.ShowLoadScreen(load.progress) + if load.done { + singleton.currentScreen = singleton.loadingScreen + singleton.loadingScreen = nil + d2gui.ShowCursor() + d2gui.HideLoadScreen() + } + case singleton.nextScreen != nil: if handler, ok := singleton.currentScreen.(ScreenUnloadHandler); ok { if err := handler.OnUnload(); err != nil { return err @@ -45,28 +69,19 @@ func Advance(elapsed float64) error { d2ui.Reset() d2gui.SetLayout(nil) - if _, ok := singleton.nextScreen.(ScreenLoadHandler); ok { + if handler, ok := singleton.nextScreen.(ScreenLoadHandler); ok { d2gui.ShowLoadScreen(0) d2gui.HideCursor() + singleton.loadingState = LoadingState{updates: make(chan loadingUpdate)} + go handler.OnLoad(singleton.loadingState) singleton.currentScreen = nil singleton.loadingScreen = singleton.nextScreen } else { singleton.currentScreen = singleton.nextScreen singleton.loadingScreen = nil } - singleton.nextScreen = nil - } else if singleton.loadingScreen != nil { - handler := singleton.loadingScreen.(ScreenLoadHandler) - if err := handler.OnLoad(); err != nil { - return err - } - - singleton.currentScreen = singleton.loadingScreen - singleton.loadingScreen = nil - d2gui.ShowCursor() - d2gui.HideLoadScreen() - } else if singleton.currentScreen != nil { + case singleton.currentScreen != nil: if handler, ok := singleton.currentScreen.(ScreenAdvanceHandler); ok { if err := handler.Advance(elapsed); err != nil { return err @@ -86,3 +101,32 @@ func Render(surface d2render.Surface) error { return nil } + +type LoadingState struct { + updates chan loadingUpdate +} + +type loadingUpdate struct { + progress float64 + err error + done bool +} + +// Error provides a way for callers to report an error during loading. +// This is meant to be delivered via the progress channel in OnLoad implementations. +func (l *LoadingState) Error(err error) { + l.updates <- loadingUpdate{err: err} +} + +// Progress provides a way for callers to report the ratio between `0` and `1` of the progress made loading a screen. +// This is meant to be delivered via the progress channel in OnLoad implementations. +func (l *LoadingState) Progress(ratio float64) { + l.updates <- loadingUpdate{progress: ratio} +} + +// Done provides a way for callers to report that screen loading has been completed. +// This is meant to be delivered via the progress channel in OnLoad implementations. +func (l *LoadingState) Done() { + l.updates <- loadingUpdate{progress: 1.0} + l.updates <- loadingUpdate{done: true} +} diff --git a/d2game/d2gamescreen/blizzard_intro.go b/d2game/d2gamescreen/blizzard_intro.go index fcdff08e..5c7ddc7c 100644 --- a/d2game/d2gamescreen/blizzard_intro.go +++ b/d2game/d2gamescreen/blizzard_intro.go @@ -3,6 +3,7 @@ package d2gamescreen import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2video" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" ) type BlizzardIntro struct { @@ -13,12 +14,15 @@ func CreateBlizzardIntro() *BlizzardIntro { return &BlizzardIntro{} } -func (v *BlizzardIntro) OnLoad() error { +func (v *BlizzardIntro) OnLoad(loading d2screen.LoadingState) { videoBytes, err := d2asset.LoadFile("/data/local/video/BlizNorth640x480.bik") if err != nil { - return err + loading.Error(err) + return } + loading.Progress(0.5) v.videoDecoder = d2video.CreateBinkDecoder(videoBytes) - return nil + loading.Done() + return } diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index b0a96241..016cfb1c 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -55,9 +55,10 @@ func CreateCharacterSelect(connectionType d2clientconnectiontype.ClientConnectio } } -func (v *CharacterSelect) OnLoad() error { +func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { d2audio.PlayBGM(d2resource.BGMTitle) d2input.BindHandler(v) + loading.Progress(0.1) animation, _ := d2asset.LoadAnimation(d2resource.CharacterSelectionBackground, d2resource.PaletteSky) v.background, _ = d2ui.LoadSprite(animation) @@ -82,6 +83,7 @@ func (v *CharacterSelect) OnLoad() error { v.exitButton.SetPosition(33, 537) v.exitButton.OnActivated(func() { v.onExitButtonClicked() }) d2ui.AddWidget(&v.exitButton) + loading.Progress(0.2) v.deleteCharCancelButton = d2ui.CreateButton(d2ui.ButtonTypeOkCancel, "NO") v.deleteCharCancelButton.SetPosition(282, 308) @@ -103,6 +105,7 @@ func (v *CharacterSelect) OnLoad() error { v.d2HeroTitle = d2ui.CreateLabel(d2resource.Font42, d2resource.PaletteUnits) v.d2HeroTitle.SetPosition(320, 23) v.d2HeroTitle.Alignment = d2ui.LabelAlignCenter + loading.Progress(0.3) v.deleteCharConfirmLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) lines := d2common.SplitIntoLinesWithMaxWidth("Are you sure that you want to delete this character? Take note: this will delete all versions of this Character.", 29) @@ -121,6 +124,7 @@ func (v *CharacterSelect) OnLoad() error { v.charScrollbar = d2ui.CreateScrollbar(586, 87, 369) v.charScrollbar.OnActivated(func() { v.onScrollUpdate() }) d2ui.AddWidget(&v.charScrollbar) + loading.Progress(0.5) for i := 0; i < 8; i++ { xOffset := 115 @@ -138,7 +142,7 @@ func (v *CharacterSelect) OnLoad() error { } v.refreshGameStates() - return nil + loading.Done() } func (v *CharacterSelect) onScrollUpdate() { diff --git a/d2game/d2gamescreen/credits.go b/d2game/d2gamescreen/credits.go index f8aa6007..65886110 100644 --- a/d2game/d2gamescreen/credits.go +++ b/d2game/d2gamescreen/credits.go @@ -63,28 +63,35 @@ func (v *Credits) LoadContributors() []string { return contributors } -// Load is called to load the resources for the credits screen -func (v *Credits) OnLoad() error { +// OnLoad is called to load the resources for the credits screen +func (v *Credits) OnLoad(loading d2screen.LoadingState) { animation, _ := d2asset.LoadAnimation(d2resource.CreditsBackground, d2resource.PaletteSky) v.creditsBackground, _ = d2ui.LoadSprite(animation) v.creditsBackground.SetPosition(0, 0) + loading.Progress(0.2) v.exitButton = d2ui.CreateButton(d2ui.ButtonTypeMedium, "EXIT") v.exitButton.SetPosition(33, 543) v.exitButton.OnActivated(func() { v.onExitButtonClicked() }) d2ui.AddWidget(&v.exitButton) + loading.Progress(0.4) fileData, err := d2asset.LoadFile(d2resource.CreditsText) if err != nil { - return err + loading.Error(err) + return } + loading.Progress(0.6) + creditData, _ := d2common.Utf16BytesToString(fileData[2:]) v.creditsText = strings.Split(creditData, "\r\n") for i := range v.creditsText { v.creditsText[i] = strings.Trim(v.creditsText[i], " ") } + loading.Progress(0.8) + v.creditsText = append(v.LoadContributors(), v.creditsText...) - return nil + loading.Done() } // Render renders the credits screen diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 53e13681..5e726fce 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -4,6 +4,8 @@ import ( "fmt" "image/color" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" @@ -40,9 +42,9 @@ func CreateGame(gameClient *d2client.GameClient) *Game { return result } -func (v *Game) OnLoad() error { +func (v *Game) OnLoad(loading d2screen.LoadingState) { d2audio.PlayBGM("") - return nil + loading.Done() } func (v *Game) OnUnload() error { diff --git a/d2game/d2gamescreen/gui_testing.go b/d2game/d2gamescreen/gui_testing.go index 3a2867ce..d8c89db1 100644 --- a/d2game/d2gamescreen/gui_testing.go +++ b/d2game/d2gamescreen/gui_testing.go @@ -3,6 +3,7 @@ package d2gamescreen import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" ) type GuiTestMain struct{} @@ -11,8 +12,9 @@ func CreateGuiTestMain() *GuiTestMain { return &GuiTestMain{} } -func (g *GuiTestMain) OnLoad() error { +func (g *GuiTestMain) OnLoad(loading d2screen.LoadingState) { layout := d2gui.CreateLayout(d2gui.PositionTypeHorizontal) + loading.Progress(0.3) // layoutLeft := layout.AddLayout(d2gui.PositionTypeVertical) layoutLeft.SetHorizontalAlign(d2gui.HorizontalAlignCenter) @@ -23,6 +25,7 @@ func (g *GuiTestMain) OnLoad() error { layoutLeft.AddLabel("FontStyleFormal10Static", d2gui.FontStyleFormal10Static) layoutLeft.AddLabel("FontStyleFormal11Units", d2gui.FontStyleFormal11Units) layoutLeft.AddLabel("FontStyleFormal12Static", d2gui.FontStyleFormal12Static) + loading.Progress(0.6) layout.AddSpacerDynamic() @@ -33,11 +36,12 @@ func (g *GuiTestMain) OnLoad() error { layoutRight.AddButton("OkCancel", d2gui.ButtonStyleOkCancel) layoutRight.AddButton("Short", d2gui.ButtonStyleShort) layoutRight.AddButton("Wide", d2gui.ButtonStyleWide) + loading.Progress(0.9) layout.SetVerticalAlign(d2gui.VerticalAlignMiddle) d2gui.SetLayout(layout) - return nil + loading.Done() } func (g *GuiTestMain) Render(screen d2render.Surface) error { diff --git a/d2game/d2gamescreen/main_menu.go b/d2game/d2gamescreen/main_menu.go index 80dc3c80..1274e5d8 100644 --- a/d2game/d2gamescreen/main_menu.go +++ b/d2game/d2gamescreen/main_menu.go @@ -80,8 +80,9 @@ func CreateMainMenu() *MainMenu { } // Load is called to load the resources for the main menu -func (v *MainMenu) OnLoad() error { +func (v *MainMenu) OnLoad(loading d2screen.LoadingState) { d2audio.PlayBGM(d2resource.BGMTitle) + loading.Progress(0.2) v.versionLabel = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.versionLabel.Alignment = d2ui.LabelAlignRight @@ -100,6 +101,7 @@ func (v *MainMenu) OnLoad() error { v.copyrightLabel.SetText("Diablo 2 is © Copyright 2000-2016 Blizzard Entertainment") v.copyrightLabel.Color = color.RGBA{R: 188, G: 168, B: 140, A: 255} v.copyrightLabel.SetPosition(400, 500) + loading.Progress(0.3) v.copyrightLabel2 = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel2.Alignment = d2ui.LabelAlignCenter @@ -112,6 +114,7 @@ func (v *MainMenu) OnLoad() error { v.openDiabloLabel.SetText("OpenDiablo2 is neither developed by, nor endorsed by Blizzard or its parent company Activision") v.openDiabloLabel.Color = color.RGBA{R: 255, G: 255, B: 140, A: 255} v.openDiabloLabel.SetPosition(400, 580) + loading.Progress(0.5) animation, _ := d2asset.LoadAnimation(d2resource.GameSelectScreen, d2resource.PaletteSky) v.background, _ = d2ui.LoadSprite(animation) @@ -130,6 +133,7 @@ func (v *MainMenu) OnLoad() error { v.diabloLogoLeft.SetBlend(true) v.diabloLogoLeft.PlayForward() v.diabloLogoLeft.SetPosition(400, 120) + loading.Progress(0.6) animation, _ = d2asset.LoadAnimation(d2resource.Diablo2LogoFireRight, d2resource.PaletteUnits) v.diabloLogoRight, _ = d2ui.LoadSprite(animation) @@ -158,6 +162,7 @@ func (v *MainMenu) OnLoad() error { v.cinematicsButton = d2ui.CreateButton(d2ui.ButtonTypeShort, "CINEMATICS") v.cinematicsButton.SetPosition(401, 505) d2ui.AddWidget(&v.cinematicsButton) + loading.Progress(0.7) v.singlePlayerButton = d2ui.CreateButton(d2ui.ButtonTypeWide, "SINGLE PLAYER") v.singlePlayerButton.SetPosition(264, 290) @@ -203,6 +208,7 @@ func (v *MainMenu) OnLoad() error { v.btnTcpIpJoinGame.SetPosition(264, 320) v.btnTcpIpJoinGame.OnActivated(func() { v.onTcpIpJoinGameClicked() }) d2ui.AddWidget(&v.btnTcpIpJoinGame) + loading.Progress(0.8) v.tcpIpOptionsLabel = d2ui.CreateLabel(d2resource.Font42, d2resource.PaletteUnits) v.tcpIpOptionsLabel.SetPosition(400, 23) @@ -224,6 +230,7 @@ func (v *MainMenu) OnLoad() error { v.tcpJoinGameEntry.SetPosition(318, 245) v.tcpJoinGameEntry.SetFilter("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890._:") d2ui.AddWidget(&v.tcpJoinGameEntry) + loading.Progress(0.9) v.btnServerIpCancel = d2ui.CreateButton(d2ui.ButtonTypeOkCancel, "CANCEL") v.btnServerIpCancel.SetPosition(285, 305) @@ -243,7 +250,7 @@ func (v *MainMenu) OnLoad() error { d2input.BindHandler(v) - return nil + loading.Done() } func (v *MainMenu) onMapTestClicked() { diff --git a/d2game/d2gamescreen/map_engine_testing.go b/d2game/d2gamescreen/map_engine_testing.go index 1ae5f3e2..9f57c9dd 100644 --- a/d2game/d2gamescreen/map_engine_testing.go +++ b/d2game/d2gamescreen/map_engine_testing.go @@ -143,13 +143,16 @@ func (met *MapEngineTest) LoadRegionByIndex(n int, levelPreset, fileIndex int) { met.mapRenderer.MoveCameraTo(met.mapRenderer.WorldToOrtho(met.mapEngine.GetCenterPosition())) } -func (met *MapEngineTest) OnLoad() error { +func (met *MapEngineTest) OnLoad(loading d2screen.LoadingState) { d2input.BindHandler(met) + loading.Progress(0.2) met.mapEngine = d2mapengine.CreateMapEngine() + loading.Progress(0.5) met.mapRenderer = d2maprenderer.CreateMapRenderer(met.mapEngine) + loading.Progress(0.7) met.LoadRegionByIndex(met.currentRegion, met.levelPreset, met.fileIndex) - return nil + loading.Done() } func (met *MapEngineTest) OnUnload() error { diff --git a/d2game/d2gamescreen/select_hero_class.go b/d2game/d2gamescreen/select_hero_class.go index a43b2f1f..ba954a61 100644 --- a/d2game/d2gamescreen/select_hero_class.go +++ b/d2game/d2gamescreen/select_hero_class.go @@ -78,8 +78,9 @@ func CreateSelectHeroClass(connectionType d2clientconnectiontype.ClientConnectio return result } -func (v *SelectHeroClass) OnLoad() error { +func (v *SelectHeroClass) OnLoad(loading d2screen.LoadingState) { d2audio.PlayBGM(d2resource.BGMTitle) + loading.Progress(0.1) v.bgImage = loadSprite(d2resource.CharacterSelectBackground, d2resource.PaletteFechar) v.bgImage.SetPosition(0, 0) @@ -97,6 +98,7 @@ func (v *SelectHeroClass) OnLoad() error { v.heroDesc1Label = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroDesc1Label.Alignment = d2ui.LabelAlignCenter v.heroDesc1Label.SetPosition(400, 100) + loading.Progress(0.3) v.heroDesc2Label = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroDesc2Label.Alignment = d2ui.LabelAlignCenter @@ -128,6 +130,7 @@ func (v *SelectHeroClass) OnLoad() error { v.heroNameLabel.Color = color.RGBA{R: 216, G: 196, B: 128, A: 255} v.heroNameLabel.SetText("Character Name") v.heroNameLabel.SetPosition(321, 475) + loading.Progress(0.4) v.heroNameTextbox = d2ui.CreateTextbox() v.heroNameTextbox.SetPosition(318, 493) @@ -155,6 +158,7 @@ func (v *SelectHeroClass) OnLoad() error { v.hardcoreCharLabel.Color = color.RGBA{R: 216, G: 196, B: 128, A: 255} v.hardcoreCharLabel.SetText("Hardcore") v.hardcoreCharLabel.SetPosition(339, 548) + loading.Progress(0.5) v.heroRenderInfo[d2enum.HeroBarbarian] = &HeroRenderInfo{ d2enum.HeroStanceIdle, @@ -234,6 +238,7 @@ func (v *SelectHeroClass) OnLoad() error { v.heroRenderInfo[d2enum.HeroSorceress].BackWalkSpriteOverlay.PlayForward() v.heroRenderInfo[d2enum.HeroSorceress].BackWalkSpriteOverlay.SetPlayLengthMs(1200) v.heroRenderInfo[d2enum.HeroSorceress].BackWalkSpriteOverlay.SetPlayLoop(false) + loading.Progress(0.6) v.heroRenderInfo[d2enum.HeroNecromancer] = &HeroRenderInfo{ d2enum.HeroStanceIdle, @@ -314,6 +319,7 @@ func (v *SelectHeroClass) OnLoad() error { v.heroRenderInfo[d2enum.HeroPaladin].BackWalkSprite.PlayForward() v.heroRenderInfo[d2enum.HeroPaladin].BackWalkSprite.SetPlayLengthMs(1300) v.heroRenderInfo[d2enum.HeroPaladin].BackWalkSprite.SetPlayLoop(false) + loading.Progress(0.7) v.heroRenderInfo[d2enum.HeroAmazon] = &HeroRenderInfo{ d2enum.HeroStanceIdle, @@ -378,6 +384,7 @@ func (v *SelectHeroClass) OnLoad() error { v.heroRenderInfo[d2enum.HeroAssassin].BackWalkSprite.PlayForward() v.heroRenderInfo[d2enum.HeroAssassin].BackWalkSprite.SetPlayLengthMs(1500) v.heroRenderInfo[d2enum.HeroAssassin].BackWalkSprite.SetPlayLoop(false) + loading.Progress(0.8) v.heroRenderInfo[d2enum.HeroDruid] = &HeroRenderInfo{ d2enum.HeroStanceIdle, @@ -411,7 +418,7 @@ func (v *SelectHeroClass) OnLoad() error { v.heroRenderInfo[d2enum.HeroDruid].BackWalkSprite.SetPlayLengthMs(1500) v.heroRenderInfo[d2enum.HeroDruid].BackWalkSprite.SetPlayLoop(false) - return nil + loading.Done() } func (v *SelectHeroClass) OnUnload() error { diff --git a/d2game/d2player/escape_menu.go b/d2game/d2player/escape_menu.go index 9f5789dc..3753d684 100644 --- a/d2game/d2player/escape_menu.go +++ b/d2game/d2player/escape_menu.go @@ -49,7 +49,6 @@ func NewEscapeMenu() *EscapeMenu { } } -// ScreenLoadHandler func (m *EscapeMenu) OnLoad() error { m.labels = []d2ui.Label{ d2ui.CreateLabel(d2resource.Font42, d2resource.PaletteSky), @@ -82,7 +81,6 @@ func (m *EscapeMenu) OnLoad() error { return nil } -// ScreenRenderHandler func (m *EscapeMenu) Render(target d2render.Surface) error { if !m.isOpen { return nil @@ -113,7 +111,6 @@ func (m *EscapeMenu) Render(target d2render.Surface) error { return nil } -// ScreenAdvanceHandler func (m *EscapeMenu) Advance(elapsed float64) error { if !m.isOpen { return nil