diff --git a/block_player.go b/block_player.go new file mode 100644 index 0000000..e3e904a --- /dev/null +++ b/block_player.go @@ -0,0 +1,52 @@ +package sii + +func init() { + RegisterBlock(&Player{}) +} + +type Player struct { + HQCity string `sii:"hq_city"` + Trailers []Ptr `sii:"trailers"` + TrailerUtilizationLogs []Ptr `sii:"trailer_utilization_logs"` + TrailerDefs []Ptr `sii:"trailer_defs"` + AssignedTruck Ptr `sii:"assigned_truck"` + MyTruck Ptr `sii:"my_truck"` + MyTruckPlacement Placement `sii:"my_truck_placement"` + MyTruckPlacementValid bool `sii:"my_truck_placement_valid"` + MyTrailerPlacement Placement `sii:"my_trailer_placement"` + MySlaveTrailerPlacements []Placement `sii:"my_slave_trailer_placements"` + MyTrailerAttached bool `sii:"my_trailer_attached"` + MyTrailerUsed bool `sii:"my_trailer_used"` + AssignedTrailer Ptr `sii:"assigned_trailer"` + MyTrailer Ptr `sii:"my_trailer"` + AssignedTrailerConnected bool `sii:"assigned_trailer_connected"` + TruckPlacement Placement `sii:"truck_placement"` + TrailerPlacement Placement `sii:"trailer_placement"` + SlaveTrailerPlacements []Placement `sii:"slave_trailer_placements"` + ScheduleTransferToHQ bool `sii:"schedule_transfer_to_hq"` + Flags uint64 `sii:"flags"` // ???? + GasPumpMoneyDebt int64 `sii:"gas_pump_money_debt"` + CurrentJob Ptr `sii:"current_job"` + CurrentBusJob Ptr `sii:"current_bus_job"` + SelectedJob Ptr `sii:"selected_job"` + DrivingTime int64 `sii:"driving_time"` + SleepingCount int `sii:"sleeping_count"` + FreeRoamDistance int64 `sii:"free_roam_distance"` + DiscoveryDistance float64 `sii:"discovary_distance"` // Typo is intended and copied from real save-game + DismissedDrivers int `sii:"dismissed_drivers"` + Trucks []Ptr `sii:"trucks"` + TruckProfitLogs []Ptr `sii:"truck_profit_logs"` + Drivers []Ptr `sii:"drivers"` + DriverReadinessTimer []int64 `sii:"driver_readiness_timer"` + DriverQuitWarned []bool `sii:"driver_quit_warned"` + + blockName string +} + +func (Player) Class() string { return "player" } + +func (p *Player) Init(class, name string) { + p.blockName = name +} + +func (p Player) Name() string { return p.blockName } diff --git a/block_save_container.go b/block_save_container.go index a76cfdf..b1fe718 100644 --- a/block_save_container.go +++ b/block_save_container.go @@ -7,7 +7,7 @@ func init() { type SaveContainer struct { SaveName string `sii:"name"` Time int64 `sii:"time"` - FileTime uint64 `sii:"file_time"` + FileTime int64 `sii:"file_time"` Version int `sii:"version"` Dependencies []string `sii:"dependencies"` diff --git a/datatypes.go b/datatypes.go new file mode 100644 index 0000000..570fe2f --- /dev/null +++ b/datatypes.go @@ -0,0 +1,60 @@ +package sii + +import "strings" + +// See https://modding.scssoft.com/wiki/Documentation/Engine/Units + +// string => native type string + +// float => native type float + +// float2-4 => [2]float - [4]float + +type Placement struct { + X, Y, Z float64 + W, X2, Y2, Z2 float64 +} + +// TODO: Implement marshalling for Placement + +// fixed => native type int + +// fixed2-4 => [2]int - [4]int + +// int2 => [2]int + +// quaternion => [4]float + +// s16, s32, s64 => int16, int32, int64 + +// u16, u32, u64 => uint16, uint32, uint64 + +// bool => native type bool + +// token => native type string + +type Ptr struct { + Target string + unit *Unit +} + +func (p Ptr) CanResolve() bool { return strings.HasPrefix(p.Target, ".") } +func (p Ptr) MarshalSII() []byte { return []byte(p.Target) } +func (p Ptr) Resolve() Block { + if p.Target == "null" { + return nil + } + + for _, b := range p.unit.Entries { + if b.Name() == p.Target { + return b + } + } + return nil +} +func (p *Ptr) UnmarshalSII(in []byte) error { + p.Target = string(in) + return nil +} + +// resource_tie => native type string diff --git a/genericMarshaller.go b/genericMarshaller.go new file mode 100644 index 0000000..85fb7f5 --- /dev/null +++ b/genericMarshaller.go @@ -0,0 +1,182 @@ +package sii + +import ( + "bufio" + "bytes" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +var ( + singleLineValue = `^\s*%s\s?:\s?(.+)$` + arrayLineValue = `^\s*%s(\[([0-9]*)\])?\s?:\s?(.+)$` +) + +func genericMarshal(in interface{}) ([]byte, error) { + return nil, errors.New("Not implemented") +} + +func genericUnmarshal(in []byte, out interface{}) error { + if reflect.TypeOf(out).Kind() != reflect.Ptr { + return errors.New("Calling parser with non-pointer") + } + + if reflect.ValueOf(out).Elem().Kind() != reflect.Struct { + return errors.New("Calling parser with pointer to non-struct") + } + + st := reflect.ValueOf(out).Elem() + for i := 0; i < st.NumField(); i++ { + valField := st.Field(i) + typeField := st.Type().Field(i) + + attributeName := typeField.Tag.Get("sii") + if attributeName == "" { + // Names must be explicitly defined, library does not support name guessing + continue + } + + switch typeField.Type { + + case reflect.TypeOf(Ptr{}): + // TODO: Implement + continue + + case reflect.TypeOf(Placement{}): + // TODO: Implement + continue + + } + + switch typeField.Type.Kind() { + case reflect.Bool: + v, err := strconv.ParseBool(string(getSingleValue(in, attributeName))) + if err != nil { + return errors.Wrapf(err, "Unable to parse boolean for attribute %q", attributeName) + } + valField.SetBool(v) + + case reflect.Float64: + v, err := strconv.ParseFloat(string(getSingleValue(in, attributeName)), 64) + if err != nil { + return errors.Wrapf(err, "Unable to parse float for attribute %q", attributeName) + } + valField.SetFloat(v) + + case reflect.Int, reflect.Int64: + v, err := strconv.ParseInt(string(getSingleValue(in, attributeName)), 10, 64) + if err != nil { + return errors.Wrapf(err, "Unable to parse int for attribute %q", attributeName) + } + valField.SetInt(v) + + case reflect.String: + v := strings.Trim(string(getSingleValue(in, attributeName)), `"`) + valField.SetString(v) + + case reflect.Uint64: + v, err := strconv.ParseUint(string(getSingleValue(in, attributeName)), 10, 64) + if err != nil { + return errors.Wrapf(err, "Unable to parse uint for attribute %q", attributeName) + } + valField.SetUint(v) + + case reflect.Slice: + ba, err := getArrayValues(in, attributeName) + if err != nil { + return errors.Wrapf(err, "Unable to fetch array values for attribute %q", attributeName) + } + + switch typeField.Type.Elem().Kind() { + case reflect.Bool: + var v []bool + for _, bv := range ba { + pbv, err := strconv.ParseBool(string(bv)) + if err != nil { + return errors.Wrapf(err, "Unable to parse boolean for attribute %q", attributeName) + } + v = append(v, pbv) + } + valField.Set(reflect.ValueOf(v)) + + case reflect.Int: + var v []int + for _, bv := range ba { + pbv, err := strconv.Atoi(string(bv)) + if err != nil { + return errors.Wrapf(err, "Unable to parse int for attribute %q", attributeName) + } + v = append(v, pbv) + } + valField.Set(reflect.ValueOf(v)) + + case reflect.String: + var v []string + for _, bv := range ba { + v = append(v, strings.Trim(string(bv), `"`)) + } + valField.Set(reflect.ValueOf(v)) + + default: + return errors.Errorf("Unsupported type: []%s", typeField.Type.Elem().Kind()) + } + + default: + return errors.Errorf("Unsupported type: %s", typeField.Type.Kind()) + } + } + + return nil +} + +func getSingleValue(in []byte, name string) []byte { + rex := regexp.MustCompile(fmt.Sprintf(singleLineValue, name)) + + var scanner = bufio.NewScanner(bytes.NewReader(in)) + for scanner.Scan() { + if rex.Match(scanner.Bytes()) { + grp := rex.FindSubmatch(scanner.Bytes()) + return grp[1] + } + } + return nil +} + +func getArrayValues(in []byte, name string) ([][]byte, error) { + rex := regexp.MustCompile(fmt.Sprintf(arrayLineValue, name)) + var out [][]byte + + var scanner = bufio.NewScanner(bytes.NewReader(in)) + for scanner.Scan() { + if rex.Match(scanner.Bytes()) { + grp := rex.FindSubmatch(scanner.Bytes()) + if len(grp[1]) == 0 { + arrayLen, err := strconv.Atoi(string(grp[3])) + if err != nil { + return nil, errors.Wrap(err, "Unable to parse array capacity") + } + out = make([][]byte, arrayLen) + continue + } + + if len(grp[2]) == 0 { + out = append(out, grp[3]) + continue + } + + idx, err := strconv.Atoi(string(grp[2])) + if err != nil { + return nil, errors.Wrap(err, "Unable to parse array index") + } + + out[idx] = grp[3] + } + } + + return out, nil +} diff --git a/parser.go b/parser.go index b8d2abb..f957b9f 100644 --- a/parser.go +++ b/parser.go @@ -89,6 +89,9 @@ func parseSIIPlainFile(r io.Reader) (*Unit, error) { case strings.HasSuffix(line, `*/`): inComment = false + case strings.HasPrefix(line, `@include`): + return nil, errors.New("File uses includes (unsupported feature)") + default: if inComment { // Inside multi-line-comment, just drop @@ -125,7 +128,7 @@ func processBlock(unit *Unit, blockClass, blockName string, blockContent []byte) if reflect.TypeOf(block).Implements(reflect.TypeOf((*Unmarshaler)(nil)).Elem()) { err = block.(Unmarshaler).UnmarshalSII(blockContent) } else { - // TODO: Add generic unmarshal + err = genericUnmarshal(blockContent, block) } if err != nil { diff --git a/parser_test.go b/parser_test.go index de31f79..5b7690f 100644 --- a/parser_test.go +++ b/parser_test.go @@ -7,16 +7,8 @@ import ( var testSii = `SiiNunit { -/* - * Multi-line comment - */ some_unit : .my_mod.unit { - // Single line comment - # Single line comment - /* - * In-Block multi-line string - */ attribute_number: 40 attribute_string: "TEST STRING" attribute_unit: test.unit