It's a complete mess for now - many things are out of place or shown when they shouldn't be - and we can't move around the game map. But, it's a good start.
352 lines
7.9 KiB
Go
352 lines
7.9 KiB
Go
package menus
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"io/ioutil"
|
|
"log"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/data"
|
|
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
|
)
|
|
|
|
type MenuType int
|
|
|
|
const (
|
|
TypeStatic MenuType = 0
|
|
TypeMenu MenuType = 1
|
|
TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra}
|
|
TypeSimpleButton MenuType = 3
|
|
TypeDoorHotspot MenuType = 30 // Like a button I guess? "FONTTYPE is animation speed"
|
|
TypeDoorHotspot2 MenuType = 31 // Seems like a duplicate of the above? What's different?
|
|
TypeLineKbd MenuType = 40
|
|
TypeThumb MenuType = 45 // A "thumb" appears to be a vertical slider
|
|
TypeLineBriefing MenuType = 41
|
|
TypeInvokeButton MenuType = 50
|
|
TypeDoorHotspot3 MenuType = 60 // Maybe? Appears in Arrange.mnu
|
|
TypeOverlay MenuType = 61
|
|
TypeHypertext MenuType = 70
|
|
TypeCheckbox MenuType = 91
|
|
TypeEditBox MenuType = 100
|
|
TypeInventorySelect MenuType = 110
|
|
TypeRadioButton MenuType = 120
|
|
TypeDropdownButton MenuType = 200
|
|
TypeComboBoxItem MenuType = 205
|
|
TypeAnimationSample MenuType = 220
|
|
TypeAnimationHover MenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
|
|
TypeMainButton MenuType = 228
|
|
TypeSlider MenuType = 232
|
|
TypeStatusBar MenuType = 233
|
|
TypeDialogue MenuType = 300
|
|
|
|
TypeListBoxUp MenuType = 400 // FIXME: these have multiple items in MENUTYPE
|
|
TypeListBoxDown MenuType = 405
|
|
)
|
|
|
|
// FIXME: certain elements - especially overlays - don't have a DESC specified
|
|
// in the .mnu file, but display text specified with a number in i18n. The only
|
|
// conclusion I can draw is that they're hardcoded in the binary and set from
|
|
// outside. So, do that here.
|
|
var DescOverrides = map[string]int{
|
|
"main:2.6": 50992,
|
|
"newgame:2.5": 50993,
|
|
"keyboard:3.3": 50995,
|
|
"levelply:2.6": 50996,
|
|
}
|
|
|
|
// FIXME: Same idea with text overrides, only these aren't mentioned in the .dta
|
|
// file at all!
|
|
var TextOverrides = map[string]string{
|
|
"main:2.7": "0.1-ordoor",
|
|
}
|
|
|
|
// FIXME: The menu is specified as type 2 (button) in these cases, which is
|
|
// weird. Make it a menu for now.
|
|
//
|
|
// Hypothesis: MENUTYPE and SUBMENUTYPE are not equivalent?
|
|
var TypeOverrides = map[string]MenuType{
|
|
"levelply:2": TypeMenu,
|
|
"savegame:2": TypeMenu,
|
|
"loadgame:2": TypeMenu,
|
|
|
|
// "thumb" is not a background.
|
|
"maingame:2": TypeStatic,
|
|
|
|
// ???
|
|
"configure_ultequip:7.5": TypeListBoxUp,
|
|
"configure_ultequip:7.6": TypeListBoxDown,
|
|
}
|
|
|
|
type Record struct {
|
|
Menu *Menu
|
|
Parent *Record
|
|
Children []*Record
|
|
|
|
Id int
|
|
Type MenuType
|
|
DrawType int
|
|
FontType int
|
|
Active bool
|
|
SpriteId []int
|
|
Share int
|
|
X int
|
|
Y int
|
|
|
|
// From i18n
|
|
Text string
|
|
Help string
|
|
|
|
// FIXME: turn these into first-class data
|
|
properties map[string]string
|
|
}
|
|
|
|
type Menu struct {
|
|
Name string
|
|
// TODO: load these
|
|
ObjectFiles []string
|
|
FontNames []string
|
|
|
|
BackgroundColor color.Color
|
|
HypertextColor color.Color
|
|
|
|
// FIXME: turn these into first-class data
|
|
Properties map[string]string
|
|
|
|
// The actual menu records. There are multiple top-level items. Submenus are
|
|
// only ever nested one deep.
|
|
Records []*Record
|
|
}
|
|
|
|
func LoadMenu(filename string) (*Menu, error) {
|
|
name := filepath.Base(filename)
|
|
name = strings.Replace(name, filepath.Ext(name), "", -1)
|
|
name = strings.ToLower(name)
|
|
|
|
// FIXME: this needs turning into a real parser sometime
|
|
scanner, err := asciiscan.New(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer scanner.Close()
|
|
|
|
var str string
|
|
var record *Record
|
|
|
|
section := 0
|
|
isProp := false
|
|
out := &Menu{
|
|
Name: name,
|
|
Properties: map[string]string{},
|
|
}
|
|
|
|
for {
|
|
str, err = scanner.ConsumeString()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Whether the lines are properties or not alternate with each section,
|
|
// except the records use `*` as a separator
|
|
if section < 3 && isProp != asciiscan.IsProperty(str) {
|
|
section += 1
|
|
isProp = !isProp
|
|
}
|
|
|
|
if str == "~" {
|
|
break
|
|
}
|
|
|
|
switch section {
|
|
case 0: // List of object files
|
|
out.ObjectFiles = append(out.ObjectFiles, str)
|
|
case 1: // List of properties
|
|
k, v := asciiscan.ConsumeProperty(str)
|
|
vInt, err := strconv.Atoi(v) // FIXME:
|
|
switch k {
|
|
case "BACKGROUND COLOR 0..255..-1 trans":
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out.BackgroundColor = data.ColorPalette[vInt]
|
|
case "HYPERTEXT COLOR 0..255":
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out.HypertextColor = data.ColorPalette[vInt]
|
|
default:
|
|
out.Properties[k] = v
|
|
}
|
|
case 2: // list of fonts
|
|
// FIXME: do we need to do something cleverer here?
|
|
if str == "NULL" {
|
|
continue
|
|
}
|
|
out.FontNames = append(out.FontNames, str)
|
|
case 3: // Menu records
|
|
if str == "*" { // NEXT RECORD
|
|
out.Records = append(out.Records, record.Toplevel())
|
|
continue
|
|
}
|
|
|
|
k, v := asciiscan.ConsumeProperty(str)
|
|
switch k {
|
|
case "MENUID":
|
|
record = newRecord(out, nil)
|
|
case "SUBMENUID":
|
|
record = newRecord(out, record.Toplevel())
|
|
}
|
|
setProperty(record, k, v)
|
|
}
|
|
}
|
|
|
|
log.Printf("Menu properties: %#+v", out.Properties)
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func LoadMenus(dir string) (map[string]*Menu, error) {
|
|
fis, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := make(map[string]*Menu, len(fis))
|
|
|
|
for _, fi := range fis {
|
|
relname := fi.Name()
|
|
basename := filepath.Base(relname)
|
|
extname := filepath.Ext(relname)
|
|
|
|
// Skip anything that isn't a .mnu file
|
|
if !strings.EqualFold(extname, ".mnu") {
|
|
continue
|
|
}
|
|
|
|
built, err := LoadMenu(filepath.Join(dir, relname))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
|
|
}
|
|
|
|
out[basename] = built
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func newRecord(menu *Menu, parent *Record) *Record {
|
|
out := &Record{
|
|
Menu: menu,
|
|
Parent: parent,
|
|
properties: map[string]string{},
|
|
}
|
|
|
|
if parent != nil {
|
|
parent.Children = append(parent.Children, out)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func (r *Record) Toplevel() *Record {
|
|
if r.Parent != nil {
|
|
return r.Parent.Toplevel()
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func setProperty(r *Record, k, v string) {
|
|
vSplit := strings.Split(v, ",")
|
|
vInt, _ := strconv.Atoi(v)
|
|
vSplitInt := make([]int, len(vSplit))
|
|
|
|
for i, subV := range vSplit {
|
|
vSplitInt[i], _ = strconv.Atoi(subV)
|
|
}
|
|
|
|
switch k {
|
|
case "MENUID", "SUBMENUID":
|
|
r.Id = vInt
|
|
case "MENUTYPE", "SUBMENUTYPE":
|
|
if strings.Contains(v, ",") {
|
|
r.Type = MenuType(vSplitInt[0]) // FIXME: what are the other values in this case?
|
|
} else {
|
|
r.Type = MenuType(vInt)
|
|
}
|
|
|
|
// FIXME: Type override. Note that MENUID is specified first, so this works
|
|
if override, ok := TypeOverrides[r.Locator()]; ok {
|
|
r.Type = override
|
|
}
|
|
case "ACTIVE":
|
|
r.Active = (vInt != 0)
|
|
case "SPRITEID":
|
|
r.SpriteId = vSplitInt
|
|
case "X-CORD":
|
|
r.X = vInt
|
|
case "Y-CORD":
|
|
r.Y = vInt
|
|
case "FONTTYPE":
|
|
r.FontType = vInt
|
|
case "DRAW TYPE":
|
|
r.DrawType = vInt
|
|
case "SHARE":
|
|
r.Share = vInt
|
|
default:
|
|
r.properties[k] = v
|
|
}
|
|
}
|
|
|
|
type Replacer interface {
|
|
ReplaceText(int, *string)
|
|
ReplaceHelp(int, *string)
|
|
}
|
|
|
|
func (r *Record) Internationalize(replacer Replacer) {
|
|
if override, ok := TextOverrides[r.Locator()]; ok {
|
|
delete(r.properties, "DESC")
|
|
r.Text = override
|
|
}
|
|
|
|
if override, ok := DescOverrides[r.Locator()]; ok {
|
|
r.properties["DESC"] = strconv.Itoa(override)
|
|
}
|
|
|
|
id, err := strconv.Atoi(r.properties["DESC"])
|
|
if err == nil {
|
|
delete(r.properties, "DESC")
|
|
replacer.ReplaceText(id, &r.Text)
|
|
replacer.ReplaceHelp(id, &r.Help)
|
|
}
|
|
|
|
for _, child := range r.Children {
|
|
child.Internationalize(replacer)
|
|
}
|
|
}
|
|
|
|
func (m *Menu) Internationalize(replacer Replacer) {
|
|
for _, record := range m.Records {
|
|
record.Internationalize(replacer)
|
|
}
|
|
}
|
|
|
|
func (r *Record) Path() string {
|
|
var path []string
|
|
|
|
for rec := r; rec != nil; rec = rec.Parent {
|
|
path = append([]string{strconv.Itoa(rec.Id)}, path...)
|
|
}
|
|
|
|
return strings.Join(path, ".")
|
|
}
|
|
|
|
func (r *Record) Locator() string {
|
|
return fmt.Sprintf("%v:%v", r.Menu.Name, r.Path())
|
|
}
|