package messages import ( "fmt" "github.com/inkyblackness/imgui-go/v3" "github.com/inkyblackness/hacked/editor/external" "github.com/inkyblackness/hacked/editor/graphics" "github.com/inkyblackness/hacked/editor/render" "github.com/inkyblackness/hacked/editor/values" "github.com/inkyblackness/hacked/ss1/content/audio" "github.com/inkyblackness/hacked/ss1/content/movie" "github.com/inkyblackness/hacked/ss1/content/text" "github.com/inkyblackness/hacked/ss1/edit/undoable/cmd" "github.com/inkyblackness/hacked/ss1/resource" "github.com/inkyblackness/hacked/ss1/world" "github.com/inkyblackness/hacked/ss1/world/ids" "github.com/inkyblackness/hacked/ui/gui" ) type messageInfo struct { title string } var knownMessageTypes = map[resource.ID]messageInfo{ ids.MailsStart: {title: "Mails"}, ids.LogsStart: {title: "Logs"}, ids.FragmentsStart: {title: "Fragments"}, } var knownMessageTypesOrder = []resource.ID{ids.MailsStart, ids.LogsStart, ids.FragmentsStart} var knownMailTypes = []string{ "Audio/Text", "VMail: 'Shield' (0)", "VMail: 'Grove' (1)", "VMail: 'Bridge' (2)", "VMail: 'Laser' (3, 4)", "VMail: 'Status' (11)", "VMail: 'Explode' (5..9)", } const videoMailDisplayBase = 256 // View provides edit controls for messages. type View struct { mod *world.Mod messageCache *text.ElectronicMessageCache cp text.Codepage movieCache *movie.Cache imageCache *graphics.TextureCache modalStateMachine gui.ModalStateMachine clipboard external.Clipboard guiScale float32 commander cmd.Commander model viewModel } // NewMessagesView returns a new instance. func NewMessagesView(mod *world.Mod, messageCache *text.ElectronicMessageCache, cp text.Codepage, movieCache *movie.Cache, imageCache *graphics.TextureCache, modalStateMachine gui.ModalStateMachine, clipboard external.Clipboard, guiScale float32, commander cmd.Commander) *View { view := &View{ mod: mod, messageCache: messageCache, cp: cp, movieCache: movieCache, imageCache: imageCache, modalStateMachine: modalStateMachine, clipboard: clipboard, guiScale: guiScale, commander: commander, model: freshViewModel(), } return view } // WindowOpen returns the flag address, to be used with the main menu. func (view *View) WindowOpen() *bool { return &view.model.windowOpen } // Render renders the view. func (view *View) Render() { if view.model.restoreFocus { imgui.SetNextWindowFocus() view.model.restoreFocus = false view.model.windowOpen = true } if view.model.windowOpen { imgui.SetNextWindowSizeV(imgui.Vec2{X: 640 * view.guiScale, Y: 480 * view.guiScale}, imgui.ConditionFirstUseEver) if imgui.BeginV("Messages", view.WindowOpen(), imgui.WindowFlagsNoCollapse) { view.renderContent() } imgui.End() } } func (view *View) renderContent() { if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: -200 * view.guiScale}, false, 0) { imgui.PushItemWidth(-150 * view.guiScale) if imgui.BeginCombo("Message Type", knownMessageTypes[view.model.currentKey.ID].title) { for _, id := range knownMessageTypesOrder { if imgui.SelectableV(knownMessageTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) { view.model.currentKey.ID = id view.model.currentKey.Index = 0 } } imgui.EndCombo() } info, _ := ids.Info(view.model.currentKey.ID) index := view.model.currentKey.Index if gui.StepSliderInt("Index", &index, 0, info.MaxCount-1) { view.model.currentKey.Index = index } if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) { languages := resource.Languages() for _, lang := range languages { if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) { view.model.currentKey.Lang = lang } } imgui.EndCombo() } message, msgReadOnly := view.currentMessage() availableDisplays := view.availableDisplays() if imgui.ButtonV("Clear All", imgui.Vec2{}) { view.requestClearAll() } imgui.SameLine() if !msgReadOnly { if imgui.ButtonV("Remove All", imgui.Vec2{}) { view.requestRemoveAll() } } else { imgui.Text("(read-only)") } if view.hasAudio() { imgui.PushID("audio") imgui.Separator() sound, soundReadOnly := view.currentSound() if !sound.Empty() { imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration())) if imgui.Button("Export") { view.requestExportAudio(sound) } if !soundReadOnly { imgui.SameLine() if imgui.Button("Remove") { view.requestRemoveAudio() } } imgui.SameLine() } else { imgui.LabelText("Audio", "(no sound)") } if imgui.Button("Import") { view.requestImportAudio() } imgui.SameLine() if imgui.Button("Clear") { view.requestClearAudio() } imgui.PopID() } imgui.Separator() textModes := map[bool]string{true: "Verbose", false: "Terse"} if imgui.BeginCombo("Text Mode", textModes[view.model.showVerboseText]) { for _, setting := range []bool{true, false} { if imgui.SelectableV(textModes[setting], view.model.showVerboseText == setting, 0, imgui.Vec2{}) { view.model.showVerboseText = setting } } imgui.EndCombo() } view.renderText(false, "Title", message.Title, func(newValue string) { view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Title = text.Blocked(newValue)[0] }) }) view.renderText(false, "Sender", message.Sender, func(newValue string) { view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Sender = text.Blocked(newValue)[0] }) }) view.renderText(false, "Subject", message.Subject, func(newValue string) { view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Subject = text.Blocked(newValue)[0] }) }) nextMessageIndex := values.NewUnifier() nextMessageIndex.Add(message.NextMessage) values.RenderUnifiedSliderInt(false, "Next Message Index", nextMessageIndex, func(u values.Unifier) int { return u.Unified().(int) }, func(int) string { return "%d" }, -1, 255, func(newValue int) { view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.NextMessage = newValue }) }) isInterrupt := values.NewUnifier() isInterrupt.Add(message.IsInterrupt) values.RenderUnifiedCheckboxCombo(false, "Is Interrupt", isInterrupt, func(newValue bool) { view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.IsInterrupt = newValue }) }) colorIndex := values.NewUnifier() colorIndex.Add(message.ColorIndex) values.RenderUnifiedSliderInt(false, "Color Index", colorIndex, func(u values.Unifier) int { return u.Unified().(int) }, func(int) string { return "%d" }, -1, 255, func(newValue int) { view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.ColorIndex = newValue }) }) if view.model.currentKey.ID == ids.MailsStart { mailType := message.LeftDisplay - videoMailDisplayBase + 1 selectedType := fmt.Sprintf("Unknown (0x%03X)", message.LeftDisplay) if mailType < 0 { mailType = 0 } if mailType < len(knownMailTypes) { selectedType = knownMailTypes[mailType] } if msgReadOnly { imgui.LabelText("Mail Type", selectedType) } else if imgui.BeginCombo("Mail Type", selectedType) { for typeIndex, typeString := range knownMailTypes { if imgui.SelectableV(typeString, mailType == typeIndex, 0, imgui.Vec2{}) { newIndex := -1 if typeIndex > 0 { newIndex = videoMailDisplayBase + (typeIndex - 1) } view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.LeftDisplay = newIndex }) } } imgui.EndCombo() } } if message.LeftDisplay < videoMailDisplayBase { view.renderDisplayGallery(false, "Left Display", message.LeftDisplay, availableDisplays, func(newValue int) { view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.LeftDisplay = newValue }) }) view.renderDisplayGallery(false, "Right Display", message.RightDisplay, availableDisplays, func(newValue int) { view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.RightDisplay = newValue }) }) } imgui.PopItemWidth() } imgui.EndChild() view.renderMFDs() } func (view *View) renderMFDs() { message, _ := view.currentMessage() view.renderSideImage("LeftMFD", message.LeftDisplay) imgui.SameLineV(0, 0) textToDisplay := message.VerboseText if imgui.BeginChildV("Text", imgui.Vec2{X: -250 * view.guiScale, Y: 190 * view.guiScale}, true, 0) { if !view.model.showVerboseText { textToDisplay = message.TerseText } imgui.PushTextWrapPos() if len(textToDisplay) == 0 { imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5}) imgui.Text("(empty)") imgui.PopStyleColor() } else { imgui.Text(textToDisplay) } imgui.PopTextWrapPos() } imgui.EndChild() view.clipboardPopup(false, "Text", textToDisplay, func(newValue string) { view.requestTextChange(func(msg *text.ElectronicMessage) { if view.model.showVerboseText { msg.VerboseText = newValue } else { msg.TerseText = newValue } }) }) imgui.SameLineV(0, 0) view.renderSideImage("RightMFD", message.RightDisplay) } func (view View) currentMessage() (msg text.ElectronicMessage, readOnly bool) { msg = view.messageOf(view.model.currentKey) readOnly = len(view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index))) == 0 return } func (view View) hasAudio() bool { return (view.model.currentKey.ID == ids.MailsStart) || (view.model.currentKey.ID == ids.LogsStart) } func (view *View) currentSound() (audio.L8, bool) { if view.hasAudio() { key := view.model.currentKey key.ID = key.ID.Plus(300) sound, _ := view.movieCache.Audio(key) readOnly := len(view.mod.ModifiedBlocks(key.Lang, key.ID.Plus(view.model.currentKey.Index))) == 0 return sound, readOnly } return audio.L8{}, true } func (view View) messageOf(key resource.Key) text.ElectronicMessage { msg, cacheErr := view.messageCache.Message(key) if cacheErr != nil { msg = text.EmptyElectronicMessage() } return msg } func (view *View) availableDisplays() int { selector := view.mod.LocalizedResources(view.model.currentKey.Lang) res, err := selector.Select(ids.MfdDataBitmaps) if err != nil { return 0 } return res.BlockCount() } func (view *View) renderText(readOnly bool, label string, value string, changeCallback func(string)) { imgui.LabelText(label, value) view.clipboardPopup(readOnly, label, value, changeCallback) } func (view *View) clipboardPopup(readOnly bool, label string, value string, changeCallback func(string)) { if imgui.BeginPopupContextItemV(label+"-Popup", 1) { if imgui.Selectable("Copy to Clipboard") { view.clipboard.SetString(value) } if !readOnly && imgui.Selectable("Copy from Clipboard") { newValue, err := view.clipboard.String() if err == nil { changeCallback(newValue) } } imgui.EndPopup() } } func (view *View) renderDisplayGallery(readOnly bool, label string, index, availableDisplays int, changeCallback func(int)) { unifier := values.NewUnifier() unifier.Add(index) values.RenderUnifiedSliderInt(readOnly, label, unifier, func(u values.Unifier) int { return u.Unified().(int) }, func(int) string { return "%d" }, -1, availableDisplays-1, changeCallback) render.TextureSelector("###"+label+" Texture", -1, view.guiScale, availableDisplays, index, view.imageCache, func(index int) resource.Key { return resource.KeyOf(ids.MfdDataBitmaps, view.model.currentKey.Lang, index) }, func(index int) string { return fmt.Sprintf("%d", index) }, changeCallback) } func (view *View) renderSideImage(label string, index int) { size := imgui.Vec2{X: 250 * view.guiScale, Y: 190 * view.guiScale} imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: size.X, Y: -size.Y}) if imgui.BeginChildV(label, size, false, 0) { if index >= 0 { key := resource.KeyOf(ids.MfdDataBitmaps, view.model.currentKey.Lang, index) render.TextureImage(label, view.imageCache, key, size) } } imgui.PopStyleVar() imgui.EndChild() } func (view *View) requestExportAudio(sound audio.L8) { filename := fmt.Sprintf("%05d_%s.wav", view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(300).Value(), view.model.currentKey.Lang.String()) external.ExportAudio(view.modalStateMachine, filename, sound) } func (view *View) requestRemoveAudio() { view.requestAudioChange(nil) } func (view *View) requestImportAudio() { external.ImportAudio(view.modalStateMachine, func(sound audio.L8) { movieData := movie.ContainSoundData(sound) view.requestAudioChange(movieData) }) } func (view *View) requestClearAudio() { view.requestAudioChange(view.silence()) } func (view View) silence() []byte { silence := audio.L8{SampleRate: 22050, Samples: []byte{0x80}} return movie.ContainSoundData(silence) } func (view *View) requestClearAll() { view.requestWipe(text.EmptyElectronicMessage().Encode(view.cp), view.silence()) } func (view *View) requestRemoveAll() { view.requestWipe(nil, nil) } func (view *View) requestWipe(newTextData [][]byte, newAudioData []byte) { textEntries := make(map[resource.Language]messageDataEntry) audioEntries := make(map[resource.Language]messageDataEntry) textID := view.model.currentKey.ID.Plus(view.model.currentKey.Index) for lang := resource.Language(0); lang < resource.LanguageCount; lang++ { textEntries[lang] = messageDataEntry{ oldData: view.mod.ModifiedBlocks(lang, textID), newData: newTextData, } if view.hasAudio() { audioEntries[lang] = messageDataEntry{ oldData: view.mod.ModifiedBlocks(lang, textID.Plus(300)), newData: [][]byte{newAudioData}, } } } view.requestSetMessageData(textEntries, audioEntries) } func (view *View) requestTextChange(modifier func(*text.ElectronicMessage)) { msg := view.messageOf(view.model.currentKey) entries := make(map[resource.Language]messageDataEntry) modifier(&msg) entries[view.model.currentKey.Lang] = messageDataEntry{ oldData: view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index)), newData: msg.Encode(view.cp), } view.requestSetMessageData(entries, nil) } func (view *View) requestPropertyChange(modifier func(*text.ElectronicMessage)) { entries := make(map[resource.Language]messageDataEntry) for lang := resource.Language(0); lang < resource.LanguageCount; lang++ { key := view.model.currentKey key.Lang = lang msg := view.messageOf(key) modifier(&msg) entries[lang] = messageDataEntry{ oldData: view.mod.ModifiedBlocks(lang, key.ID.Plus(key.Index)), newData: msg.Encode(view.cp), } } view.requestSetMessageData(entries, nil) } func (view *View) requestAudioChange(movieData []byte) { entries := make(map[resource.Language]messageDataEntry) entries[view.model.currentKey.Lang] = messageDataEntry{ oldData: view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(300)), newData: [][]byte{movieData}, } view.requestSetMessageData(nil, entries) } func (view *View) requestSetMessageData(textEntries map[resource.Language]messageDataEntry, audioEntries map[resource.Language]messageDataEntry) { command := setMessageDataCommand{ key: view.model.currentKey, showVerboseText: view.model.showVerboseText, model: &view.model, textEntries: textEntries, audioEntries: audioEntries, } view.commander.Queue(command) }