2018-12-30 23:23:08 +00:00
|
|
|
package menus
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-03-30 00:15:19 +01:00
|
|
|
"image/color"
|
2018-12-30 23:23:08 +00:00
|
|
|
"io/ioutil"
|
2020-03-30 00:15:19 +01:00
|
|
|
"log"
|
2018-12-30 23:23:08 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-03-30 00:15:19 +01:00
|
|
|
"code.ur.gs/lupine/ordoor/internal/data"
|
2019-12-31 01:55:58 +00:00
|
|
|
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
2018-12-30 23:23:08 +00:00
|
|
|
)
|
|
|
|
|
2020-03-22 22:12:59 +00:00
|
|
|
type MenuType int
|
|
|
|
|
2020-03-21 18:50:26 +00:00
|
|
|
const (
|
2020-03-22 22:12:59 +00:00
|
|
|
TypeStatic MenuType = 0
|
|
|
|
TypeMenu MenuType = 1
|
2020-03-25 00:23:28 +00:00
|
|
|
TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra}
|
2020-03-24 20:21:55 +00:00
|
|
|
TypeSimpleButton MenuType = 3
|
2020-03-27 02:07:28 +00:00
|
|
|
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?
|
2020-03-25 00:23:28 +00:00
|
|
|
TypeLineKbd MenuType = 40
|
|
|
|
TypeThumb MenuType = 45
|
|
|
|
TypeLineBriefing MenuType = 41
|
2020-03-22 22:12:59 +00:00
|
|
|
TypeInvokeButton MenuType = 50
|
2020-03-27 02:07:28 +00:00
|
|
|
TypeDoorHotspot3 MenuType = 60 // Maybe? Appears in Arrange.mnu
|
2020-03-22 22:12:59 +00:00
|
|
|
TypeOverlay MenuType = 61
|
|
|
|
TypeHypertext MenuType = 70
|
|
|
|
TypeCheckbox MenuType = 91
|
2020-03-25 00:23:28 +00:00
|
|
|
TypeEditBox MenuType = 100
|
|
|
|
TypeInventorySelect MenuType = 110
|
|
|
|
TypeRadioButton MenuType = 120
|
|
|
|
TypeDropdownButton MenuType = 200
|
|
|
|
TypeComboBoxItem MenuType = 205
|
2020-03-22 22:12:59 +00:00
|
|
|
TypeAnimationSample MenuType = 220
|
2020-03-27 00:54:57 +00:00
|
|
|
TypeAnimationHover MenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
|
2020-03-22 22:12:59 +00:00
|
|
|
TypeMainButton MenuType = 228
|
|
|
|
TypeSlider MenuType = 232
|
2020-03-25 00:23:28 +00:00
|
|
|
TypeStatusBar MenuType = 233
|
|
|
|
TypeDialogue MenuType = 300
|
2020-03-21 18:50:26 +00:00
|
|
|
)
|
|
|
|
|
2020-03-26 23:35:34 +00:00
|
|
|
// 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]map[string]int{
|
|
|
|
"main": {
|
|
|
|
"2.6": 50992,
|
|
|
|
},
|
|
|
|
"newgame": {
|
|
|
|
"2.5": 50993,
|
|
|
|
},
|
|
|
|
"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]map[string]string{
|
|
|
|
"main": {
|
|
|
|
"2.7": "0.1-ordoor",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-12-30 23:23:08 +00:00
|
|
|
type Record struct {
|
2020-03-26 22:09:26 +00:00
|
|
|
Menu *Menu
|
2018-12-30 23:23:08 +00:00
|
|
|
Parent *Record
|
|
|
|
Children []*Record
|
|
|
|
|
|
|
|
Id int
|
2020-03-22 22:12:59 +00:00
|
|
|
Type MenuType
|
2020-03-21 18:50:26 +00:00
|
|
|
DrawType int
|
2019-10-09 00:41:41 +01:00
|
|
|
FontType int
|
2018-12-30 23:23:08 +00:00
|
|
|
Active bool
|
2019-10-09 00:41:41 +01:00
|
|
|
SpriteId []int
|
2020-03-21 18:50:26 +00:00
|
|
|
Share int
|
2018-12-30 23:23:08 +00:00
|
|
|
X int
|
|
|
|
Y int
|
2020-03-26 22:09:26 +00:00
|
|
|
|
|
|
|
// From i18n
|
2020-03-26 23:35:34 +00:00
|
|
|
Text string
|
|
|
|
Help string
|
2018-12-30 23:23:08 +00:00
|
|
|
|
|
|
|
// FIXME: turn these into first-class data
|
|
|
|
properties map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Menu struct {
|
|
|
|
Name string
|
|
|
|
// TODO: load these
|
|
|
|
ObjectFiles []string
|
|
|
|
FontNames []string
|
|
|
|
|
2020-03-30 00:15:19 +01:00
|
|
|
BackgroundColor color.Color
|
|
|
|
HypertextColor color.Color
|
|
|
|
|
2018-12-30 23:23:08 +00:00
|
|
|
// 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)
|
2020-03-26 22:09:26 +00:00
|
|
|
name = strings.Replace(name, filepath.Ext(name), "", -1)
|
|
|
|
name = strings.ToLower(name)
|
2018-12-30 23:23:08 +00:00
|
|
|
|
|
|
|
// FIXME: this needs turning into a real parser sometime
|
|
|
|
scanner, err := asciiscan.New(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-01-02 04:38:03 +00:00
|
|
|
defer scanner.Close()
|
|
|
|
|
2018-12-30 23:23:08 +00:00
|
|
|
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)
|
2020-03-30 00:15:19 +01:00
|
|
|
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
|
|
|
|
}
|
2018-12-30 23:23:08 +00:00
|
|
|
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":
|
2020-03-26 22:09:26 +00:00
|
|
|
record = newRecord(out, nil)
|
2018-12-30 23:23:08 +00:00
|
|
|
case "SUBMENUID":
|
2020-03-26 22:09:26 +00:00
|
|
|
record = newRecord(out, record.Toplevel())
|
2018-12-30 23:23:08 +00:00
|
|
|
}
|
|
|
|
setProperty(record, k, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-30 00:15:19 +01:00
|
|
|
log.Printf("Menu properties: %#+v", out.Properties)
|
|
|
|
|
2018-12-30 23:23:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-03-26 22:09:26 +00:00
|
|
|
func newRecord(menu *Menu, parent *Record) *Record {
|
2018-12-30 23:23:08 +00:00
|
|
|
out := &Record{
|
2020-03-26 22:09:26 +00:00
|
|
|
Menu: menu,
|
2018-12-30 23:23:08 +00:00
|
|
|
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) {
|
2019-10-09 00:41:41 +01:00
|
|
|
vSplit := strings.Split(v, ",")
|
2018-12-30 23:23:08 +00:00
|
|
|
vInt, _ := strconv.Atoi(v)
|
2019-10-09 00:41:41 +01:00
|
|
|
vSplitInt := make([]int, len(vSplit))
|
|
|
|
|
|
|
|
for i, subV := range vSplit {
|
|
|
|
vSplitInt[i], _ = strconv.Atoi(subV)
|
|
|
|
}
|
|
|
|
|
2018-12-30 23:23:08 +00:00
|
|
|
switch k {
|
|
|
|
case "MENUID", "SUBMENUID":
|
|
|
|
r.Id = vInt
|
|
|
|
case "MENUTYPE", "SUBMENUTYPE":
|
2020-03-22 22:12:59 +00:00
|
|
|
r.Type = MenuType(vInt)
|
2018-12-30 23:23:08 +00:00
|
|
|
case "ACTIVE":
|
|
|
|
r.Active = (vInt != 0)
|
|
|
|
case "SPRITEID":
|
2019-10-09 00:41:41 +01:00
|
|
|
r.SpriteId = vSplitInt
|
2018-12-30 23:23:08 +00:00
|
|
|
case "X-CORD":
|
|
|
|
r.X = vInt
|
|
|
|
case "Y-CORD":
|
|
|
|
r.Y = vInt
|
2019-10-09 00:41:41 +01:00
|
|
|
case "FONTTYPE":
|
|
|
|
r.FontType = vInt
|
2020-03-21 18:50:26 +00:00
|
|
|
case "DRAW TYPE":
|
|
|
|
r.DrawType = vInt
|
|
|
|
case "SHARE":
|
|
|
|
r.Share = vInt
|
2018-12-30 23:23:08 +00:00
|
|
|
default:
|
|
|
|
r.properties[k] = v
|
|
|
|
}
|
|
|
|
}
|
2019-01-02 06:16:15 +00:00
|
|
|
|
|
|
|
type Replacer interface {
|
2020-03-26 22:09:26 +00:00
|
|
|
ReplaceText(int, *string)
|
|
|
|
ReplaceHelp(int, *string)
|
2019-01-02 06:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Record) Internationalize(replacer Replacer) {
|
2020-03-26 23:35:34 +00:00
|
|
|
if overrides, ok := TextOverrides[r.Menu.Name]; ok {
|
|
|
|
if override, ok := overrides[r.Path()]; ok {
|
|
|
|
delete(r.properties, "DESC")
|
|
|
|
r.Text = override
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if overrides, ok := DescOverrides[r.Menu.Name]; ok {
|
|
|
|
if override, ok := overrides[r.Path()]; ok {
|
|
|
|
r.properties["DESC"] = strconv.Itoa(override)
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
id, err := strconv.Atoi(r.properties["DESC"])
|
2019-01-02 06:16:15 +00:00
|
|
|
if err == nil {
|
2020-03-26 22:09:26 +00:00
|
|
|
delete(r.properties, "DESC")
|
|
|
|
replacer.ReplaceText(id, &r.Text)
|
|
|
|
replacer.ReplaceHelp(id, &r.Help)
|
2019-01-02 06:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, child := range r.Children {
|
|
|
|
child.Internationalize(replacer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Menu) Internationalize(replacer Replacer) {
|
|
|
|
for _, record := range m.Records {
|
|
|
|
record.Internationalize(replacer)
|
|
|
|
}
|
|
|
|
}
|
2020-03-23 00:33:29 +00:00
|
|
|
|
|
|
|
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, ".")
|
|
|
|
}
|