mirror of
https://github.com/Luzifer/sii.git
synced 2024-12-20 16:11:17 +00:00
Implement Save-Game Editor (#1)
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
57e2be2495
commit
ab39609f45
31 changed files with 2415 additions and 0 deletions
1
cmd/sii-editor/.gitignore
vendored
Normal file
1
cmd/sii-editor/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
7
cmd/sii-editor/Makefile
Normal file
7
cmd/sii-editor/Makefile
Normal file
|
@ -0,0 +1,7 @@
|
|||
export FA_VERSION=5.12.0
|
||||
|
||||
default:
|
||||
|
||||
assets:
|
||||
bash ci/bundle_js.sh
|
||||
bash ci/fontawesome.sh
|
29
cmd/sii-editor/api.go
Normal file
29
cmd/sii-editor/api.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func apiGenericError(w http.ResponseWriter, status int, err error) {
|
||||
var eString = "undefined error"
|
||||
if err != nil {
|
||||
eString = err.Error()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"code": status,
|
||||
"error": eString,
|
||||
"success": false,
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, status, data)
|
||||
}
|
||||
|
||||
func apiGenericJSONResponse(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
81
cmd/sii-editor/api_gameinfo.go
Normal file
81
cmd/sii-editor/api_gameinfo.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/sii"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
router.HandleFunc("/api/gameinfo/cargo", handleListCargo).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}/companies", handleListCompanies).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
type commCargo struct {
|
||||
Name string `json:"name"`
|
||||
Mass float32 `json:"mass"`
|
||||
}
|
||||
|
||||
type commCompany struct {
|
||||
City string `json:"city"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func handleListCargo(w http.ResponseWriter, r *http.Request) {
|
||||
var result = map[string]commCargo{}
|
||||
|
||||
for _, b := range baseGameUnit.BlocksByClass("cargo_data") {
|
||||
c := b.(*sii.CargoData)
|
||||
var cName = c.CargoName
|
||||
if strings.HasPrefix(cName, "@@") {
|
||||
// Localization string, translate
|
||||
cName = locale.GetTranslation(cName)
|
||||
}
|
||||
|
||||
result[c.Name()] = commCargo{
|
||||
Name: cName,
|
||||
Mass: c.Mass,
|
||||
}
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func handleListCompanies(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
result = map[string]commCompany{}
|
||||
vars = mux.Vars(r)
|
||||
)
|
||||
|
||||
game, _, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range game.BlocksByClass("company") {
|
||||
c, ok := b.(*sii.Company)
|
||||
if !ok {
|
||||
// Should not happen but to be sure...
|
||||
continue
|
||||
}
|
||||
|
||||
cName := baseGameUnit.BlockByName(c.CityPtr().Target).(*sii.CityData).CityNameLocalized
|
||||
if strings.HasPrefix(cName, "@@") {
|
||||
cName = locale.GetTranslation(cName)
|
||||
if cName == strings.Trim(baseGameUnit.BlockByName(c.CityPtr().Target).(*sii.CityData).CityNameLocalized, "@") {
|
||||
cName = baseGameUnit.BlockByName(c.CityPtr().Target).(*sii.CityData).CityName
|
||||
}
|
||||
}
|
||||
|
||||
result[c.Name()] = commCompany{
|
||||
City: cName,
|
||||
Name: baseGameUnit.BlockByName(c.PermanentData.Target).(*sii.CompanyPermanent).CompanyName,
|
||||
}
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, result)
|
||||
}
|
97
cmd/sii-editor/api_profiles.go
Normal file
97
cmd/sii-editor/api_profiles.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const savePollTime = 10 * time.Second
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func init() {
|
||||
router.HandleFunc("/api/profiles", handleListProfiles).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves", handleGetProfileSaves).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func handleGetProfileSaves(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
subscribe = r.FormValue("subscribe") == "true"
|
||||
vars = mux.Vars(r)
|
||||
)
|
||||
|
||||
saves, err := listSaves(vars["profileID"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !subscribe {
|
||||
apiGenericJSONResponse(w, http.StatusOK, saves)
|
||||
return
|
||||
}
|
||||
|
||||
// Remember latest save
|
||||
var latestSave time.Time
|
||||
for _, s := range saves {
|
||||
if s.SaveTime.After(latestSave) {
|
||||
latestSave = s.SaveTime
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("Unable to open websocket")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.WriteJSON(saves); err != nil {
|
||||
log.WithError(err).Debug("Unable to send saves list")
|
||||
return
|
||||
}
|
||||
|
||||
for t := time.NewTicker(savePollTime); ; <-t.C {
|
||||
saves, err = listSaves(vars["profileID"])
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Unable to list saves during socket")
|
||||
return
|
||||
}
|
||||
|
||||
var newSaveTime time.Time
|
||||
for _, s := range saves {
|
||||
if s.SaveTime.After(latestSave) {
|
||||
newSaveTime = s.SaveTime
|
||||
}
|
||||
}
|
||||
|
||||
if newSaveTime.IsZero() {
|
||||
// Nothing new
|
||||
continue
|
||||
}
|
||||
|
||||
if err = conn.WriteJSON(saves); err != nil {
|
||||
log.WithError(err).Error("Unable to send saves list")
|
||||
return
|
||||
}
|
||||
|
||||
latestSave = newSaveTime
|
||||
}
|
||||
}
|
||||
|
||||
func handleListProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
profiles, err := listProfiles()
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, profiles)
|
||||
}
|
312
cmd/sii-editor/api_saves.go
Normal file
312
cmd/sii-editor/api_saves.go
Normal file
|
@ -0,0 +1,312 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/Luzifer/sii"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
storeSaveFolder = "sii_editor"
|
||||
storeSaveName = "SII Editor"
|
||||
)
|
||||
|
||||
func init() {
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}", handleGetSaveInfo).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}/economy", handleUpdateEconomyInfo).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}/fix", handleFixPlayer).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}/jobs", handleListJobs).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}/jobs", handleAddJob).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/profiles/{profileID}/saves/{saveFolder}/set-trailer", handleSetTrailer).Methods(http.MethodPut)
|
||||
}
|
||||
|
||||
func handleAddJob(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
job commSaveJob
|
||||
vars = mux.Vars(r)
|
||||
)
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&job); err != nil {
|
||||
apiGenericError(w, http.StatusBadRequest, errors.Wrap(err, "Unable to decode input data"))
|
||||
return
|
||||
}
|
||||
|
||||
game, info, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
info.SaveName = storeSaveName
|
||||
info.FileTime = time.Now().Unix()
|
||||
|
||||
// Set urgency if it isn't
|
||||
if job.Urgency == nil {
|
||||
u := int64(0)
|
||||
job.Urgency = &u
|
||||
}
|
||||
|
||||
if job.Weight == 0 {
|
||||
job.Weight = 10000 // 10 tons as a default
|
||||
}
|
||||
|
||||
if job.Weight < 100 {
|
||||
// User clearly did't want 54kg but 54 tons! (If not: screw em)
|
||||
job.Weight *= 1000
|
||||
}
|
||||
|
||||
if job.Distance == 0 {
|
||||
job.Distance = 100 // If the user did not provide distance use 100km as a default
|
||||
}
|
||||
|
||||
// Get company
|
||||
company := game.BlockByName(job.OriginReference).(*sii.Company)
|
||||
// Get cargo
|
||||
cargo := baseGameUnit.BlockByName(job.CargoReference).(*sii.CargoData)
|
||||
|
||||
// Get trailer / truck from other jobs
|
||||
var (
|
||||
cTruck, cTV, cTD string
|
||||
cTP []sii.Placement
|
||||
)
|
||||
|
||||
for _, jp := range company.JobOffer {
|
||||
j := jp.Resolve().(*sii.JobOfferData)
|
||||
if j.CompanyTruck.Target == "null" || j.TrailerVariant.Target == "null" || j.TrailerDefinition.Target == "null" || len(j.TrailerPlace) < 1 {
|
||||
continue
|
||||
}
|
||||
cTruck, cTV, cTD = j.CompanyTruck.Target, j.TrailerVariant.Target, j.TrailerDefinition.Target
|
||||
cTP = j.TrailerPlace
|
||||
break
|
||||
}
|
||||
|
||||
if cTP == nil {
|
||||
// The company did not have any valid job offers to steal from, lets search globally
|
||||
for _, jb := range game.BlocksByClass("job_offer_data") {
|
||||
j := jb.(*sii.JobOfferData)
|
||||
if j.CompanyTruck.Target == "null" || j.TrailerVariant.Target == "null" || j.TrailerDefinition.Target == "null" || len(j.TrailerPlace) < 1 {
|
||||
continue
|
||||
}
|
||||
cTruck, cTV, cTD = j.CompanyTruck.Target, j.TrailerVariant.Target, j.TrailerDefinition.Target
|
||||
cTP = j.TrailerPlace
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
jobID := "_nameless." + strconv.FormatInt(time.Now().Unix(), 16)
|
||||
exTime := game.BlocksByClass("economy")[0].(*sii.Economy).GameTime + 300 // 300min = 5h
|
||||
j := &sii.JobOfferData{
|
||||
// User requested job data
|
||||
Target: strings.TrimPrefix(job.TargetReference, "company.volatile."),
|
||||
ExpirationTime: &exTime,
|
||||
Urgency: job.Urgency,
|
||||
Cargo: sii.Ptr{Target: job.CargoReference},
|
||||
UnitsCount: int64(job.Weight / cargo.Mass),
|
||||
ShortestDistanceKM: job.Distance,
|
||||
|
||||
// Some static data
|
||||
FillRatio: 1, // Dunno but other jobs have it at 1, so keep for now
|
||||
|
||||
// Dunno where this data comes from, steal it from previous first job
|
||||
TrailerPlace: cTP,
|
||||
|
||||
// Too lazy to implement, just steal it too
|
||||
CompanyTruck: sii.Ptr{Target: cTruck},
|
||||
TrailerVariant: sii.Ptr{Target: cTV},
|
||||
TrailerDefinition: sii.Ptr{Target: cTD},
|
||||
}
|
||||
j.Init("", jobID)
|
||||
|
||||
// Add the new job to the game
|
||||
game.Entries = append(game.Entries, j)
|
||||
company.JobOffer = append([]sii.Ptr{{Target: j.Name()}}, company.JobOffer...)
|
||||
|
||||
// Write the save-game
|
||||
if err = storeSave(vars["profileID"], storeSaveFolder, game, info); err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to store save"))
|
||||
return
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, map[string]interface{}{"success": true})
|
||||
}
|
||||
|
||||
func handleFixPlayer(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
fixType = fixAll
|
||||
vars = mux.Vars(r)
|
||||
)
|
||||
|
||||
if v := r.FormValue("type"); v != "" {
|
||||
fixType = v
|
||||
}
|
||||
|
||||
game, info, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
info.SaveName = storeSaveName
|
||||
info.FileTime = time.Now().Unix()
|
||||
|
||||
if err = fixPlayerTruck(game, fixType); err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to apply fixes"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = storeSave(vars["profileID"], storeSaveFolder, game, info); err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to store save"))
|
||||
return
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, map[string]interface{}{"success": true})
|
||||
}
|
||||
|
||||
func handleGetSaveInfo(w http.ResponseWriter, r *http.Request) {
|
||||
var vars = mux.Vars(r)
|
||||
|
||||
game, _, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
info, err := commSaveDetailsFromUnit(game)
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to gather info"))
|
||||
return
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, info)
|
||||
}
|
||||
|
||||
func handleListJobs(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
result []commSaveJob
|
||||
vars = mux.Vars(r)
|
||||
undiscovered = r.FormValue("undiscovered") == "true"
|
||||
)
|
||||
|
||||
game, _, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
economy := game.BlocksByClass("economy")[0].(*sii.Economy)
|
||||
var visitedCities []string
|
||||
for _, p := range economy.VisitedCities {
|
||||
visitedCities = append(visitedCities, p.Target)
|
||||
}
|
||||
|
||||
for _, cb := range game.BlocksByClass("company") {
|
||||
c := cb.(*sii.Company)
|
||||
|
||||
cityName := strings.TrimPrefix(c.CityPtr().Target, "city.") // The "VisitedCities" pointers are kinda broken and do not contain the "city." part
|
||||
if !str.StringInSlice(cityName, visitedCities) && !undiscovered {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, b := range c.JobOffer {
|
||||
j := b.Resolve().(*sii.JobOfferData)
|
||||
|
||||
if j.Target == "" || *j.ExpirationTime < economy.GameTime {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, commSaveJob{
|
||||
OriginReference: c.Name(),
|
||||
TargetReference: strings.Join([]string{"company", "volatile", j.Target}, "."),
|
||||
CargoReference: j.Cargo.Target,
|
||||
Distance: j.ShortestDistanceKM,
|
||||
Urgency: j.Urgency,
|
||||
Expires: *j.ExpirationTime - economy.GameTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func handleSetTrailer(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
reference = r.FormValue("ref")
|
||||
vars = mux.Vars(r)
|
||||
)
|
||||
|
||||
game, info, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
if game.BlockByName(reference) == nil {
|
||||
apiGenericError(w, http.StatusBadRequest, errors.New("Invalid reference given"))
|
||||
return
|
||||
}
|
||||
|
||||
info.SaveName = storeSaveName
|
||||
info.FileTime = time.Now().Unix()
|
||||
|
||||
game.BlocksByClass("player")[0].(*sii.Player).AssignedTrailer = sii.Ptr{Target: reference}
|
||||
|
||||
if err = storeSave(vars["profileID"], storeSaveFolder, game, info); err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to store save"))
|
||||
return
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, map[string]interface{}{"success": true})
|
||||
}
|
||||
|
||||
func handleUpdateEconomyInfo(w http.ResponseWriter, r *http.Request) {
|
||||
var vars = mux.Vars(r)
|
||||
|
||||
game, info, err := loadSave(vars["profileID"], vars["saveFolder"])
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to load save"))
|
||||
return
|
||||
}
|
||||
|
||||
info.SaveName = storeSaveName
|
||||
info.FileTime = time.Now().Unix()
|
||||
|
||||
blocks := game.BlocksByClass("economy")
|
||||
if len(blocks) != 1 {
|
||||
// expecting exactly one economy block
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.New("Did not find economy block"))
|
||||
return
|
||||
}
|
||||
economy := blocks[0].(*sii.Economy)
|
||||
|
||||
if xpRaw := r.FormValue("xp"); xpRaw != "" {
|
||||
xp, err := strconv.ParseInt(xpRaw, 10, 64)
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusBadRequest, errors.Wrap(err, "Invalid value to xp parameter"))
|
||||
return
|
||||
}
|
||||
economy.ExperiencePoints = xp
|
||||
}
|
||||
|
||||
if moneyRaw := r.FormValue("money"); moneyRaw != "" {
|
||||
money, err := strconv.ParseInt(moneyRaw, 10, 64)
|
||||
if err != nil {
|
||||
apiGenericError(w, http.StatusBadRequest, errors.Wrap(err, "Invalid value to money parameter"))
|
||||
return
|
||||
}
|
||||
economy.Bank.Resolve().(*sii.Bank).MoneyAccount = money
|
||||
}
|
||||
|
||||
if err = storeSave(vars["profileID"], storeSaveFolder, game, info); err != nil {
|
||||
apiGenericError(w, http.StatusInternalServerError, errors.Wrap(err, "Unable to store save"))
|
||||
return
|
||||
}
|
||||
|
||||
apiGenericJSONResponse(w, http.StatusOK, map[string]interface{}{"success": true})
|
||||
}
|
83
cmd/sii-editor/base.go
Normal file
83
cmd/sii-editor/base.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/scs-extract/scs"
|
||||
"github.com/Luzifer/sii"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var baseDataFiles = regexp.MustCompile(`^def/(?:cargo|city|company)/[^/]+.sii$`)
|
||||
|
||||
func readBaseData() (*sii.Unit, error) {
|
||||
var unitData = new(bytes.Buffer)
|
||||
// Open a plain unit for parsing
|
||||
unitData.WriteString("SiiNunit\n{\n")
|
||||
|
||||
// Collect all available units from game files
|
||||
entries, err := ioutil.ReadDir(getGamePath())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to list game-directory")
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".scs") {
|
||||
// We don't care for anthing than SCS# files
|
||||
continue
|
||||
}
|
||||
|
||||
fPath := path.Join(getGamePath(), entry.Name())
|
||||
|
||||
stat, err := os.Stat(fPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to stat SCS# archive")
|
||||
}
|
||||
|
||||
scsFile, err := os.Open(fPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open SCS# archive")
|
||||
}
|
||||
defer scsFile.Close()
|
||||
|
||||
r, err := scs.NewReader(scsFile, stat.Size())
|
||||
if err != nil {
|
||||
// There are some files being unreadable, that's okay
|
||||
log.WithField("file", entry.Name()).Debug("Found unreadable SCS archive")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range r.Files {
|
||||
if !baseDataFiles.MatchString(f.Name) {
|
||||
// We don't care for most of the files, just mentioned definitions
|
||||
continue
|
||||
}
|
||||
|
||||
fr, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open file from SCS archive")
|
||||
}
|
||||
|
||||
if _, err = io.Copy(unitData, fr); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to read file from SCS archive")
|
||||
}
|
||||
|
||||
unitData.WriteString("\n") // Ensure CR after each block
|
||||
|
||||
fr.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close unit
|
||||
unitData.WriteString("}")
|
||||
|
||||
// Read-in constructed unit file
|
||||
return sii.ParseSIIPlainFile(unitData)
|
||||
}
|
20
cmd/sii-editor/ci/bundle_js.sh
Normal file
20
cmd/sii-editor/ci/bundle_js.sh
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
css_deps=(
|
||||
npm/bootstrap@4/dist/css/bootstrap.min.css
|
||||
npm/bootswatch@4/dist/darkly/bootstrap.min.css
|
||||
npm/bootstrap-vue@2/dist/bootstrap-vue.min.css
|
||||
)
|
||||
|
||||
js_deps=(
|
||||
npm/vue@2/dist/vue.min.js
|
||||
npm/bootstrap-vue@2/dist/bootstrap-vue.min.js
|
||||
npm/axios@0.19.0/dist/axios.min.js
|
||||
npm/moment@2.24.0/min/moment.min.js
|
||||
)
|
||||
|
||||
IFS=','
|
||||
|
||||
curl -sSfLo frontend/combine.js "https://cdn.jsdelivr.net/combine/${js_deps[*]}"
|
||||
curl -sSfLo frontend/combine.css "https://cdn.jsdelivr.net/combine/${css_deps[*]}"
|
13
cmd/sii-editor/ci/fontawesome.sh
Normal file
13
cmd/sii-editor/ci/fontawesome.sh
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
# Ensure deletion of older version
|
||||
rm -rf frontend/fontawesome
|
||||
|
||||
# Download and unpack fontawesome-free
|
||||
curl -sSfLo frontend/fa.zip "https://use.fontawesome.com/releases/v${FA_VERSION}/fontawesome-free-${FA_VERSION}-web.zip"
|
||||
unzip frontend/fa.zip -d frontend
|
||||
rm frontend/fa.zip
|
||||
|
||||
# Move to generic path
|
||||
mv frontend/fontawesome-free-${FA_VERSION}-web frontend/fontawesome
|
53
cmd/sii-editor/config.go
Normal file
53
cmd/sii-editor/config.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var errUserConfigNotFound = errors.New("User config not found")
|
||||
|
||||
type configFile struct {
|
||||
GameDirectories map[string]string `yaml:"game_directories"`
|
||||
ProfileDirectories map[string]string `yaml:"profile_directories"`
|
||||
}
|
||||
|
||||
func (c *configFile) loadDefaults() error {
|
||||
for k, dv := range gamePaths {
|
||||
if c.GameDirectories[k] == "" {
|
||||
c.GameDirectories[k] = dv
|
||||
}
|
||||
}
|
||||
|
||||
for k, dv := range profilePaths {
|
||||
if c.ProfileDirectories[k] == "" {
|
||||
c.ProfileDirectories[k] = dv
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadUserConfig(p string) (*configFile, error) {
|
||||
var c = &configFile{
|
||||
GameDirectories: map[string]string{},
|
||||
ProfileDirectories: map[string]string{},
|
||||
}
|
||||
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return c, errUserConfigNotFound
|
||||
}
|
||||
return c, errors.Wrap(err, "Unable to stat user config")
|
||||
}
|
||||
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return c, errors.Wrap(err, "Unable to open user config")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return c, errors.Wrap(yaml.NewDecoder(f).Decode(&c), "Unable to read user config")
|
||||
}
|
14
cmd/sii-editor/frontend.go
Normal file
14
cmd/sii-editor/frontend.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
func init() {
|
||||
router.HandleFunc("/", handleIndexPage).Methods(http.MethodGet)
|
||||
router.PathPrefix("/asset/").Handler(
|
||||
http.StripPrefix("/asset/", http.FileServer(http.Dir("frontend"))),
|
||||
).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func handleIndexPage(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "frontend/index.html")
|
||||
}
|
83
cmd/sii-editor/frontend/.eslintrc.js
Normal file
83
cmd/sii-editor/frontend/.eslintrc.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
// https://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
'root': true,
|
||||
'parserOptions': {
|
||||
parser: 'babel-eslint',
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
'env': {
|
||||
browser: true,
|
||||
},
|
||||
'extends': [
|
||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
||||
'eslint:recommended',
|
||||
],
|
||||
'globals': {
|
||||
process: true,
|
||||
Vue: true,
|
||||
axios: true,
|
||||
moment: true,
|
||||
},
|
||||
// add your custom rules here
|
||||
'rules': {
|
||||
'array-bracket-newline': ['error', { multiline: true }],
|
||||
'array-bracket-spacing': ['error'],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'arrow-parens': ['error', 'as-needed'],
|
||||
'arrow-spacing': ['error', { before: true, after: true }],
|
||||
'block-spacing': ['error'],
|
||||
'brace-style': ['error', '1tbs'],
|
||||
'comma-dangle': ['error', 'always-multiline'], // Apply Contentflow rules
|
||||
'comma-spacing': ['error'],
|
||||
'comma-style': ['error', 'last'],
|
||||
'curly': ['error'],
|
||||
'dot-location': ['error', 'property'],
|
||||
'dot-notation': ['error'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
||||
'func-call-spacing': ['error', 'never'],
|
||||
'function-paren-newline': ['error', 'multiline'],
|
||||
'generator-star-spacing': ['off'], // allow async-await
|
||||
'implicit-arrow-linebreak': ['error'],
|
||||
'indent': ['error', 2],
|
||||
'key-spacing': ['error', { beforeColon: false, afterColon: true, mode: 'strict' }],
|
||||
'keyword-spacing': ['error'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'lines-between-class-members': ['error'],
|
||||
'multiline-comment-style': ['warn'],
|
||||
'newline-per-chained-call': ['error'],
|
||||
'no-console': ['off'],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // allow debugger during development
|
||||
'no-else-return': ['error'],
|
||||
'no-extra-parens': ['error'],
|
||||
'no-implicit-coercion': ['error'],
|
||||
'no-lonely-if': ['error'],
|
||||
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 0, maxBOF: 0 }],
|
||||
'no-multi-spaces': ['error'],
|
||||
'no-trailing-spaces': ['error'],
|
||||
'no-unneeded-ternary': ['error'],
|
||||
'no-useless-return': ['error'],
|
||||
'no-whitespace-before-property': ['error'],
|
||||
'object-curly-newline': ['error', { consistent: true }],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'object-shorthand': ['error'],
|
||||
'padded-blocks': ['error', 'never'],
|
||||
'prefer-arrow-callback': ['error'],
|
||||
'prefer-const': ['error'],
|
||||
'prefer-object-spread': ['error'],
|
||||
'prefer-template': ['error'],
|
||||
'quote-props': ['error', 'consistent-as-needed', { keywords: true }],
|
||||
'quotes': ['error', 'single', { allowTemplateLiterals: true }],
|
||||
'semi': ['error', 'never'],
|
||||
'space-before-blocks': ['error', 'always'],
|
||||
'spaced-comment': ['warn', 'always'],
|
||||
'space-infix-ops': ['error'],
|
||||
'space-in-parens': ['error', 'never'],
|
||||
'space-unary-ops': ['error', { words: true, nonwords: false }],
|
||||
'switch-colon-spacing': ['error'],
|
||||
'unicode-bom': ['error', 'never'],
|
||||
'wrap-iife': ['error'],
|
||||
'yoda': ['error'],
|
||||
},
|
||||
}
|
3
cmd/sii-editor/frontend/.gitignore
vendored
Normal file
3
cmd/sii-editor/frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
combine.css
|
||||
combine.js
|
||||
fontawesome
|
31
cmd/sii-editor/frontend/app.css
Normal file
31
cmd/sii-editor/frontend/app.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
body { font-family: sans-serif; }
|
||||
|
||||
.dmg { height: 200px; }
|
||||
|
||||
.dmg svg { width: 100%; height: 100%; }
|
||||
|
||||
.dmg.truckok #truck { fill: #008000 !important; }
|
||||
.dmg.truckok #truckdmg { fill: #fff !important; }
|
||||
.dmg.truckwarn #truck { fill: #FFCC00 !important; }
|
||||
.dmg.truckwarn #truckdmg { fill: #000 !important; }
|
||||
.dmg.truckdanger #truck { fill: #CA0B00 !important; }
|
||||
.dmg.truckdanger #truckdmg { fill: #fff !important; }
|
||||
|
||||
.dmg.trailerna #trailer { fill: rgba(255, 255, 255, 0.05) !important; }
|
||||
.dmg.trailerna #trailerdmg { display: none; }
|
||||
.dmg.trailerok #trailer { fill: #008000 !important; }
|
||||
.dmg.trailerok #trailerdmg { fill: #fff !important; }
|
||||
.dmg.trailerwarn #trailer { fill: #FFCC00 !important; }
|
||||
.dmg.trailerwarn #trailerdmg { fill: #000 !important; }
|
||||
.dmg.trailerdanger #trailer { fill: #CA0B00 !important; }
|
||||
.dmg.trailerdanger #trailerdmg { fill: #fff !important; }
|
||||
|
||||
.dmg #cargo { box-shadow: 0px 0px 5px white; }
|
||||
.dmg.cargona #cargo { display: none; }
|
||||
.dmg.cargona #cargodmg { display: none; }
|
||||
.dmg.cargook #cargo { fill: #008000 !important; }
|
||||
.dmg.cargook #cargodmg { fill: #fff !important; }
|
||||
.dmg.cargowarn #cargo { fill: #FFCC00 !important; }
|
||||
.dmg.cargowarn #cargodmg { fill: #000 !important; }
|
||||
.dmg.cargodanger #cargo { fill: #CA0B00 !important; }
|
||||
.dmg.cargodanger #cargodmg { fill: #fff !important; }
|
375
cmd/sii-editor/frontend/app.js
Normal file
375
cmd/sii-editor/frontend/app.js
Normal file
|
@ -0,0 +1,375 @@
|
|||
const colorThresholds = {
|
||||
ok: 0,
|
||||
warn: 10,
|
||||
danger: 50,
|
||||
}
|
||||
|
||||
window.app = new Vue({
|
||||
|
||||
computed: {
|
||||
cargoSelectItems() {
|
||||
const result = []
|
||||
|
||||
for (const ref in this.cargo) {
|
||||
result.push({
|
||||
value: ref,
|
||||
text: `${this.cargo[ref].name}`,
|
||||
})
|
||||
}
|
||||
|
||||
return result.sort((a, b) => a.text.localeCompare(b.text))
|
||||
},
|
||||
|
||||
companySelectItems() {
|
||||
const result = []
|
||||
|
||||
for (const ref in this.companies) {
|
||||
result.push({
|
||||
value: ref,
|
||||
text: `${this.companies[ref].city}, ${this.companies[ref].name}`,
|
||||
})
|
||||
}
|
||||
|
||||
return result.sort((a, b) => a.text.localeCompare(b.text))
|
||||
},
|
||||
|
||||
dmgCargo() {
|
||||
return this.save && this.save.cargo_damage * 100 || 0
|
||||
},
|
||||
|
||||
dmgTrailer() {
|
||||
return this.save && this.save.trailer_wear * 100 || 0
|
||||
},
|
||||
|
||||
dmgTruck() {
|
||||
return this.save && this.save.truck_wear * 100 || 0
|
||||
},
|
||||
|
||||
jobFields() {
|
||||
return [
|
||||
{
|
||||
key: 'origin_reference',
|
||||
label: 'Origin',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'target_reference',
|
||||
label: 'Target',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'cargo_reference',
|
||||
label: 'Cargo',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'distance',
|
||||
label: 'Dist',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'urgency',
|
||||
label: 'Urg',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'expires',
|
||||
label: 'Exp',
|
||||
sortable: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
ownedTrailers() {
|
||||
const trailers = []
|
||||
|
||||
if (this.save && this.save.current_job && this.save.current_job.trailer_reference) {
|
||||
trailers.push({ value: this.save.current_job.trailer_reference, text: 'Company Trailer' })
|
||||
}
|
||||
|
||||
let attachedTrailerOwned = false
|
||||
for (const id in this.save.owned_trailers) {
|
||||
if (id === this.save.attached_trailer) {
|
||||
attachedTrailerOwned = true
|
||||
}
|
||||
trailers.push({ value: id, text: this.save.owned_trailers[id] })
|
||||
}
|
||||
|
||||
if (this.save && this.save.trailer_attached && !attachedTrailerOwned && (!this.save.current_job || this.save.trailer_attached !== this.save.current_job.trailer_reference)) {
|
||||
trailers.push({ value: this.save.attached_trailer, text: 'Other Trailer' })
|
||||
}
|
||||
|
||||
return trailers
|
||||
},
|
||||
|
||||
sortedSaves() {
|
||||
const saves = []
|
||||
|
||||
for (const id in this.saves) {
|
||||
const name = this.saves[id].name !== '' ? this.saves[id].name : this.saveIDToName(id)
|
||||
saves.push({
|
||||
...this.saves[id],
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
return saves.sort((b, a) => new Date(a.save_time) - new Date(b.save_time))
|
||||
},
|
||||
|
||||
truckClass() {
|
||||
const classes = ['dmg']
|
||||
|
||||
const classSelector = (prefix, value) => {
|
||||
for (const t of ['danger', 'warn', 'ok']) {
|
||||
if (value >= colorThresholds[t]) {
|
||||
return `${prefix}${t}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
classes.push(classSelector('truck', this.dmgTruck))
|
||||
|
||||
if (this.save && this.save.trailer_attached) {
|
||||
classes.push(classSelector('trailer', this.dmgTrailer))
|
||||
classes.push(classSelector('cargo', this.dmgCargo))
|
||||
} else {
|
||||
classes.push('trailerna')
|
||||
classes.push('cargona')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadCargo()
|
||||
this.loadProfiles()
|
||||
},
|
||||
|
||||
data: {
|
||||
autoLoad: false,
|
||||
cargo: {},
|
||||
companies: {},
|
||||
jobs: [],
|
||||
newJob: { weight: 10 },
|
||||
profiles: {},
|
||||
save: null,
|
||||
saveLoading: false,
|
||||
saves: {},
|
||||
selectedProfile: null,
|
||||
selectedSave: null,
|
||||
showAutosaves: false,
|
||||
showSaveModal: false,
|
||||
socket: null,
|
||||
},
|
||||
|
||||
el: '#app',
|
||||
|
||||
methods: {
|
||||
attachTrailer() {
|
||||
this.showSaveModal = true
|
||||
return axios.put(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}/set-trailer?ref=${this.save.attached_trailer}`)
|
||||
.then(() => this.showToast('Success', 'Trailer attached', 'success'))
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not attach trailer', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
createJob() {
|
||||
if (!this.companies[this.newJob.origin_reference]) {
|
||||
this.showToast('Uhm…', 'Source Company does not exist', 'danger')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.companies[this.newJob.target_reference]) {
|
||||
this.showToast('Uhm…', 'Target Company does not exist', 'danger')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.cargo[this.newJob.cargo_reference]) {
|
||||
this.showToast('Uhm…', 'Cargo does not exist', 'danger')
|
||||
return
|
||||
}
|
||||
|
||||
this.newJob.weight = parseInt(this.newJob.weight)
|
||||
if (this.newJob.weight > 200) {
|
||||
this.showToast('Uhm…', 'You want to pull more than 200 Tons of cargo?', 'danger')
|
||||
return
|
||||
}
|
||||
|
||||
this.showSaveModal = true
|
||||
|
||||
return axios.post(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}/jobs`, this.newJob)
|
||||
.then(() => {
|
||||
this.showToast('Success', 'Job created', 'success')
|
||||
this.newJob = { weight: 10 } // Reset job
|
||||
})
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not add job', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
executeRepair(fixType) {
|
||||
this.showSaveModal = true
|
||||
return axios.put(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}/fix?type=${fixType}`)
|
||||
.then(() => this.showToast('Success', 'Repair executed', 'success'))
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not repair', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
loadCargo() {
|
||||
return axios.get(`/api/gameinfo/cargo`)
|
||||
.then(resp => {
|
||||
this.cargo = resp.data
|
||||
})
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not load cargo defintion', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
loadCompanies() {
|
||||
return axios.get(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}/companies`)
|
||||
.then(resp => {
|
||||
this.companies = resp.data
|
||||
})
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not load company defintion', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
loadJobs() {
|
||||
return axios.get(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}/jobs`)
|
||||
.then(resp => {
|
||||
this.jobs = resp.data
|
||||
})
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not load jobs', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
loadNewestSave() {
|
||||
this.selectSave(this.sortedSaves[0].id, null)
|
||||
},
|
||||
|
||||
loadProfiles() {
|
||||
return axios.get('/api/profiles')
|
||||
.then(resp => {
|
||||
this.profiles = resp.data
|
||||
})
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not load profiles', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
loadSave() {
|
||||
// Load companies in background
|
||||
this.loadCompanies()
|
||||
|
||||
this.saveLoading = true
|
||||
this.showSaveModal = false
|
||||
return axios.get(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}`)
|
||||
.then(resp => {
|
||||
this.save = resp.data
|
||||
this.saveLoading = false
|
||||
})
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not load save-game', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
loadSaves() {
|
||||
if (this.socket) {
|
||||
// Dispose old socket
|
||||
this.socket.close()
|
||||
this.socket = null
|
||||
}
|
||||
|
||||
const loc = window.location
|
||||
const socketBase = `${loc.protocol === 'https:' ? 'wss:' : 'ws:'}//${loc.host}`
|
||||
this.socket = new WebSocket(`${socketBase}/api/profiles/${this.selectedProfile}/saves?subscribe=true`)
|
||||
this.socket.onclose = () => window.setTimeout(this.loadSaves, 1000) // Restart socket
|
||||
this.socket.onmessage = evt => {
|
||||
this.saves = JSON.parse(evt.data)
|
||||
if (this.autoLoad) {
|
||||
this.loadNewestSave()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
saveIDToName(id) {
|
||||
if (id === 'quicksave') {
|
||||
return 'Quicksave'
|
||||
}
|
||||
|
||||
if (id.indexOf('autosave') >= 0) {
|
||||
return 'Autosave'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
|
||||
selectProfile(profileID) {
|
||||
this.selectedProfile = profileID
|
||||
},
|
||||
|
||||
selectSave(saveID) {
|
||||
if (this.selectedSave === saveID) {
|
||||
this.loadSave()
|
||||
} else {
|
||||
this.selectedSave = saveID
|
||||
}
|
||||
},
|
||||
|
||||
setEconomy(param, value) {
|
||||
this.showSaveModal = true
|
||||
return axios.put(`/api/profiles/${this.selectedProfile}/saves/${this.selectedSave}/economy?${param}=${value}`)
|
||||
.then(() => this.showToast('Success', 'Economy updated', 'success'))
|
||||
.catch(err => {
|
||||
this.showToast('Uhoh…', 'Could not update economy', 'danger')
|
||||
console.error(err)
|
||||
})
|
||||
},
|
||||
|
||||
showToast(title, text, variant = 'info') {
|
||||
this.$bvToast.toast(text, {
|
||||
'appendToast': true,
|
||||
'autoHideDelay': 2500,
|
||||
'is-status': true,
|
||||
'solid': true,
|
||||
title,
|
||||
variant,
|
||||
})
|
||||
},
|
||||
|
||||
validInt(v) {
|
||||
return v >= 0 && v <= 2147483647
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
autoLoad() {
|
||||
if (this.autoLoad) {
|
||||
this.loadNewestSave()
|
||||
}
|
||||
},
|
||||
|
||||
selectedProfile() {
|
||||
this.loadSaves()
|
||||
},
|
||||
|
||||
selectedSave() {
|
||||
this.loadSave()
|
||||
},
|
||||
},
|
||||
|
||||
})
|
260
cmd/sii-editor/frontend/index.html
Normal file
260
cmd/sii-editor/frontend/index.html
Normal file
File diff suppressed because one or more lines are too long
1
cmd/sii-editor/frontend/truck_grp.m.svg
Normal file
1
cmd/sii-editor/frontend/truck_grp.m.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 4.9 KiB |
92
cmd/sii-editor/frontend/truck_grp.svg
Normal file
92
cmd/sii-editor/frontend/truck_grp.svg
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="760"
|
||||
height="360"
|
||||
version="1.1"
|
||||
id="svg73"
|
||||
sodipodi:docname="truck_grp.svg"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14">
|
||||
<defs
|
||||
id="defs77" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2556"
|
||||
inkscape:window-height="1393"
|
||||
id="namedview75"
|
||||
showgrid="false"
|
||||
inkscape:snap-bbox="false"
|
||||
inkscape:zoom="0.66184211"
|
||||
inkscape:cx="-129.04307"
|
||||
inkscape:cy="35.079803"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="45"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg73"
|
||||
inkscape:snap-text-baseline="false" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="truck"
|
||||
d="m 51.080083,87.484352 -39.28125,97.724608 v 92.90625 c 0,6.47406 4.522608,12.38672 12.59961,12.38672 H 46.632817 C 63.502558,228.0995 128.28394,218.82629 157.06641,271.2324 h 71.15234 c 31.72209,-49.80697 104.55521,-40.59246 108.21094,18.66016 h 20.01172 c 0,-25.06269 -27.3824,-70.27539 -57.81055,-70.27539 H 159.29102 L 160.00977,87.824196 Z M 104.38867,247.22264 c -24.572308,0.172 -44.359788,18.77732 -44.236321,41.59375 0.123467,22.81631 20.110659,41.2372 44.683591,41.17968 24.57293,-0.0573 44.461,-18.56994 44.46094,-41.38671 l -0.002,-0.41602 c -0.24693,-22.81549 -20.33393,-41.14267 -44.90625,-40.9707 z m 175.71094,0 c -24.57231,0.172 -44.35982,18.77732 -44.23633,41.59375 0.12349,22.81631 20.11066,41.2372 44.6836,41.17968 24.57293,-0.0573 44.46099,-18.56994 44.46093,-41.38671 l -0.002,-0.41602 c -0.24693,-22.81549 -20.33394,-41.14267 -44.90625,-40.9707 z"
|
||||
style="fill:#000000;fill-rule:nonzero;stroke-width:1.98951471;stroke-linecap:round;stroke-miterlimit:4;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="trailer"
|
||||
d="M 208.13477,47.953102 208.00781,207.32029 747.71094,207.2031 V 48.498024 Z M 559.54102,219.86131 c -30.42815,0 -57.81053,45.21064 -57.81055,70.27344 h 20.06445 c 3.55237,-57.57751 72.41046,-64.09942 93.6211,-38.46094 l -0.0645,2.33594 h 20.06641 l -0.0664,-2.33594 c 21.21061,-25.63848 90.06873,-19.11657 93.6211,38.46094 h 20.00195 c -3e-5,-25.0628 -27.38436,-70.27344 -57.8125,-70.27344 z m 15.06836,27.36133 c -24.57231,0.172 -44.35981,18.77732 -44.23633,41.59375 0.12349,22.81631 20.11262,41.23714 44.68554,41.17968 24.57294,-0.0573 44.45905,-18.56994 44.45899,-41.38671 l -0.002,-0.41602 c -0.24693,-22.81549 -20.33394,-41.14271 -44.90624,-40.9707 z m 98.35546,0 c -24.57231,0.172 -44.35982,18.77732 -44.23632,41.59375 0.12349,22.81631 20.11068,41.23714 44.68359,41.17968 24.57294,-0.0573 44.461,-18.56994 44.46094,-41.38671 l -0.002,-0.41602 c -0.24693,-22.81549 -20.33391,-41.14271 -44.90625,-40.9707 z"
|
||||
style="fill:#cccccc;fill-rule:nonzero;stroke-width:1.98951471;stroke-linecap:round;stroke-miterlimit:4;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none" />
|
||||
<path
|
||||
stroke-miterlimit="4"
|
||||
id="window"
|
||||
d="m 118.28646,110.25706 0.20881,60.00242 -93.545075,9.18226 26.459875,-68.9908 z"
|
||||
style="fill:#cccccc;fill-rule:nonzero;stroke-width:1.98951471;stroke-linecap:round;stroke-miterlimit:4;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="stroke-width:1;stroke:#000000;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="cargo"
|
||||
d="m 625.7003,58.018686 -69.26701,37.602085 c 0,0 22.2644,13.358649 22.01702,12.369119 l 69.76176,-37.354708 z m 52.19618,27.137148 -71.09531,39.699536 c 0,0 16.38118,9.3785 17.72406,10.33158 l 72.10706,-39.692768 c -0.16807,-0.08697 -18.73581,-10.338348 -18.73581,-10.338348 z m 20.03941,13.928297 -71.24606,38.838999 v 62.09291 l 71.74082,-38.83899 z m -142.49214,0.24737 c 0,0 1.7e-4,62.092769 -0.16475,62.010309 -0.16491,-0.0824 67.45271,38.92162 67.45271,38.92162 l 0.49476,-61.84555 -18.30628,-10.39004 -0.24737,23.25392 -7.91623,-10.39005 -7.91624,1.73167 -5.68979,-8.16361 -6.18455,1.23693 0.24738,-23.50131 z"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
y="219.30014"
|
||||
x="86.675941"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"
|
||||
id="text51"><tspan
|
||||
y="219.30014"
|
||||
x="86.675941"
|
||||
id="truckdmg"
|
||||
sodipodi:role="line">100%</tspan></text>
|
||||
<text
|
||||
y="141.94373"
|
||||
x="357.04852"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"
|
||||
id="text54"><tspan
|
||||
y="141.94373"
|
||||
x="357.04852"
|
||||
id="trailerdmg"
|
||||
sodipodi:role="line">100%</tspan></text>
|
||||
<text
|
||||
y="159.75394"
|
||||
x="659.46777"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
xml:space="preserve"
|
||||
id="text54-3"><tspan
|
||||
y="159.75394"
|
||||
x="659.46777"
|
||||
id="cargodmg"
|
||||
sodipodi:role="line"
|
||||
style="font-size:24px">100%</tspan></text>
|
||||
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
24
cmd/sii-editor/go.mod
Normal file
24
cmd/sii-editor/go.mod
Normal file
|
@ -0,0 +1,24 @@
|
|||
module github.com/Luzifer/sii/cmd/sii-editor
|
||||
|
||||
go 1.13
|
||||
|
||||
replace github.com/Luzifer/sii => ../../
|
||||
|
||||
replace github.com/Luzifer/sii/t3nk => ../../t3nk
|
||||
|
||||
require (
|
||||
github.com/Luzifer/go_helpers/v2 v2.9.1
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1
|
||||
github.com/Luzifer/scs-extract v0.1.1-0.20191226001718-17f71578850d
|
||||
github.com/Luzifer/sii v0.0.0-00010101000000-000000000000
|
||||
github.com/Luzifer/sii/t3nk v0.0.0-00010101000000-000000000000
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.7
|
||||
)
|
62
cmd/sii-editor/go.sum
Normal file
62
cmd/sii-editor/go.sum
Normal file
|
@ -0,0 +1,62 @@
|
|||
github.com/Luzifer/go_helpers/v2 v2.9.1 h1:MVUOlD6tJ2m/iTF0hllnI/QVZH5kI+TikUm1WRGg/c4=
|
||||
github.com/Luzifer/go_helpers/v2 v2.9.1/go.mod h1:ZnWxPjyCdQ4rZP3kNiMSUW/7FigU1X9Rz8XopdJ5ZCU=
|
||||
github.com/Luzifer/rconfig v2.2.0+incompatible h1:Kle3+rshPM7LxciOheaR4EfHUzibkDDGws04sefQ5m8=
|
||||
github.com/Luzifer/rconfig v2.2.0+incompatible/go.mod h1:9pet6z2+mm/UAB0jF/rf0s62USfHNolzgR6Q4KpsJI0=
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg=
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw=
|
||||
github.com/Luzifer/scs-extract v0.1.0 h1:f7fgtGZVgFiTZTWETjO92e43zzk8dXkL6zzWeRcI/IE=
|
||||
github.com/Luzifer/scs-extract v0.1.0/go.mod h1:cns55Lfhfnx1eb4mQP2mdKQBWZmOaQYVbNqJrYIJwpU=
|
||||
github.com/Luzifer/scs-extract v0.1.1-0.20191226001718-17f71578850d h1:KElMkqh5nijsan2uaw+gsdxu56NnVg/1N+Wbw3Jdy+o=
|
||||
github.com/Luzifer/scs-extract v0.1.1-0.20191226001718-17f71578850d/go.mod h1:cns55Lfhfnx1eb4mQP2mdKQBWZmOaQYVbNqJrYIJwpU=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/tenfyzhong/cityhash v0.0.0-20181130044406-4c2731b5918c/go.mod h1:Izvvi9mFtnF9nbPc2Z/gazIliNnYtxOsbQnFYpmxbfc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||
gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 h1:2QQcyaEBdpfjjYkF0MXc69jZbHb4IOYuXz2UwsmVM8k=
|
||||
gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
72
cmd/sii-editor/locale.go
Normal file
72
cmd/sii-editor/locale.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/Luzifer/scs-extract/scs"
|
||||
"github.com/Luzifer/sii"
|
||||
"github.com/Luzifer/sii/t3nk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func getLocale(locale string) (*sii.LocalizationDB, error) {
|
||||
fPath := path.Join(getGamePath(), "locale.scs")
|
||||
|
||||
stat, err := os.Stat(fPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to stat locale.scs archive")
|
||||
}
|
||||
|
||||
scsFile, err := os.Open(fPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open locale.scs archive")
|
||||
}
|
||||
defer scsFile.Close()
|
||||
|
||||
r, err := scs.NewReader(scsFile, stat.Size())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to open locale.scs archive")
|
||||
}
|
||||
|
||||
var codedLocale = new(bytes.Buffer)
|
||||
for _, f := range r.Files {
|
||||
if f.Name == path.Join("locale", locale, "local.sii") {
|
||||
lf, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to read local.sii file from archive")
|
||||
}
|
||||
defer lf.Close()
|
||||
|
||||
// Using the reader directly somehow broke mid-file, pre-buffering works fine
|
||||
if _, err := io.Copy(codedLocale, lf); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to copy local.sii file from archive")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if codedLocale.Len() == 0 {
|
||||
return nil, errors.New("Found no locale information")
|
||||
}
|
||||
|
||||
localeReader, err := t3nk.Decode(codedLocale)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to decode locale.sii")
|
||||
}
|
||||
|
||||
unit, err := sii.ParseSIIPlainFile(localeReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to read unit from locale.sii")
|
||||
}
|
||||
|
||||
for _, b := range unit.BlocksByClass("localization_db") {
|
||||
if v, ok := b.(*sii.LocalizationDB); ok {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("No localization db found in locale")
|
||||
}
|
98
cmd/sii-editor/main.go
Normal file
98
cmd/sii-editor/main.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/rconfig/v2"
|
||||
"github.com/Luzifer/sii"
|
||||
)
|
||||
|
||||
const defaultTranslation = "en_us"
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
Config string `flag:"config,c" vardefault:"config" description:"Optional configuration file"`
|
||||
DecryptKey string `flag:"decrypt-key" default:"" description:"Hex formated decryption key" validate:"nonzero"`
|
||||
Game string `flag:"game,g" default:"ets2" description:"Which game to manage (ets2 / ats)"`
|
||||
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"`
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
baseGameUnit *sii.Unit
|
||||
locale *sii.LocalizationDB
|
||||
router = mux.NewRouter()
|
||||
userConfig *configFile
|
||||
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rconfig.SetVariableDefaults(map[string]string{
|
||||
"config": userConfigPath,
|
||||
})
|
||||
|
||||
rconfig.AutoEnv(true)
|
||||
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||||
log.Fatalf("Unable to parse commandline options: %s", err)
|
||||
}
|
||||
|
||||
if cfg.VersionAndExit {
|
||||
fmt.Printf("sii-editor %s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
|
||||
log.WithError(err).Fatal("Unable to parse log level")
|
||||
} else {
|
||||
log.SetLevel(l)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
decryptKey, err := hex.DecodeString(cfg.DecryptKey)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to read encryption key")
|
||||
}
|
||||
|
||||
sii.SetEncryptionKey(decryptKey)
|
||||
|
||||
if userConfig, err = loadUserConfig(cfg.Config); err != nil && err != errUserConfigNotFound {
|
||||
log.WithError(err).Fatal("Unable to load user config")
|
||||
}
|
||||
|
||||
if err = userConfig.loadDefaults(); err != nil {
|
||||
log.WithError(err).Fatal("Unable to load missing defaults for user config")
|
||||
}
|
||||
|
||||
log.Info("Loading game base data...")
|
||||
|
||||
if baseGameUnit, err = readBaseData(); err != nil {
|
||||
log.WithError(err).Fatal("Unable to load game definitions")
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"cargos": len(baseGameUnit.BlocksByClass("cargo_data")),
|
||||
"cities": len(baseGameUnit.BlocksByClass("city_data")),
|
||||
"companies": len(baseGameUnit.BlocksByClass("company_permanent")),
|
||||
}).Info("Game base data loaded")
|
||||
|
||||
log.Info("Loading translations...")
|
||||
// TODO: Make user definable
|
||||
if locale, err = getLocale(defaultTranslation); err != nil {
|
||||
log.WithError(err).Fatal("Unable to load translations")
|
||||
}
|
||||
log.WithField("translations", len(locale.Keys)).Info("Translations loaded")
|
||||
|
||||
log.WithField("addr", cfg.Listen).Info("Starting API server...")
|
||||
|
||||
if err := http.ListenAndServe(cfg.Listen, router); err != nil {
|
||||
log.WithError(err).Fatal("HTTP server caused an error")
|
||||
}
|
||||
}
|
49
cmd/sii-editor/paths.go
Normal file
49
cmd/sii-editor/paths.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
pathETS2 = "ets2"
|
||||
pathATS = "ats"
|
||||
)
|
||||
|
||||
var errPathNotFound = errors.New("Could not find path")
|
||||
|
||||
func expandHomedir(dir string) string {
|
||||
s, err := homedir.Expand(dir)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Unable to expand home path")
|
||||
return dir
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getGamePath() string {
|
||||
return expandHomedir(userConfig.GameDirectories[cfg.Game])
|
||||
}
|
||||
|
||||
func getProfilesPath() string {
|
||||
return expandHomedir(userConfig.ProfileDirectories[cfg.Game])
|
||||
}
|
||||
|
||||
func getProfilePath(profile string) string {
|
||||
return path.Join(getProfilesPath(), profile)
|
||||
}
|
||||
|
||||
func getProfileInfoPath(profile string) string {
|
||||
return path.Join(getProfilePath(profile), "profile.sii")
|
||||
}
|
||||
|
||||
func getSavePath(profile, save string) string {
|
||||
return path.Join(getProfilePath(profile), "save", save)
|
||||
}
|
||||
|
||||
func getSaveFilePath(profile, save, file string) string {
|
||||
return path.Join(getSavePath(profile, save), file)
|
||||
}
|
15
cmd/sii-editor/paths_linux.go
Normal file
15
cmd/sii-editor/paths_linux.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
var userConfigPath = "~/.config/sii-editor/config.yml"
|
||||
|
||||
var profilePaths = map[string]string{
|
||||
// Linux default for non-Steam-profiles
|
||||
pathATS: "~/.local/share/American Truck Simulator/profiles",
|
||||
pathETS2: "~/.local/share/Euro Truck Simulator 2/profiles",
|
||||
}
|
||||
|
||||
var gamePaths = map[string]string{
|
||||
// Linux default installation path for Steam games
|
||||
pathATS: "~/.local/share/Steam/steamapps/common/American Truck Simulator",
|
||||
pathETS2: "~/.local/share/Steam/steamapps/common/Euro Truck Simulator 2",
|
||||
}
|
15
cmd/sii-editor/paths_windows.go
Normal file
15
cmd/sii-editor/paths_windows.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
var userConfigPath = `~\documents\sii-editor\config.yml`
|
||||
|
||||
var profilePaths = map[string]string{
|
||||
// Windows default for non-Steam-profiles
|
||||
pathATS: `~\documents\American Truck Simulator\profiles`,
|
||||
pathETS2: `~\documents\Euro Truck Simulator 2\profiles`,
|
||||
}
|
||||
|
||||
var gamePaths = map[string]string{
|
||||
// Windows default installation path for Steam games
|
||||
pathATS: `C:\Program Files (x86)\Steam\steamapps\common\American Truck Simulator`,
|
||||
pathETS2: `C:\Program Files (x86)\Steam\steamapps\common\Euro Truck Simulator 2`,
|
||||
}
|
123
cmd/sii-editor/profiles.go
Normal file
123
cmd/sii-editor/profiles.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/sii"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type commSaveInfo struct {
|
||||
Name string `json:"name"`
|
||||
GameTime int64 `json:"game_time"`
|
||||
SaveTime time.Time `json:"save_time"`
|
||||
}
|
||||
|
||||
func commSaveInfoFromSaveContainer(c *sii.SaveContainer) commSaveInfo {
|
||||
var out commSaveInfo
|
||||
|
||||
out.Name = c.SaveName
|
||||
out.GameTime = c.Time
|
||||
out.SaveTime = time.Unix(c.FileTime, 0)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
type commProfileInfo struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
ProfileName string `json:"profile_name"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
SaveTime time.Time `json:"save_time"`
|
||||
}
|
||||
|
||||
func commProfileInfoFromUserProfile(p *sii.UserProfile) commProfileInfo {
|
||||
var out commProfileInfo
|
||||
|
||||
out.CompanyName = p.CompanyName
|
||||
out.ProfileName = p.ProfileName
|
||||
out.CreationTime = time.Unix(p.CreationTime, 0)
|
||||
out.SaveTime = time.Unix(p.SaveTime, 0)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func listSaves(profile string) (map[string]commSaveInfo, error) {
|
||||
entries, err := ioutil.ReadDir(path.Join(getProfilePath(profile), "save"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to list saves")
|
||||
}
|
||||
|
||||
var out = map[string]commSaveInfo{}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
// There shouldn't be files but whatever
|
||||
continue
|
||||
}
|
||||
|
||||
var sFile = getSaveFilePath(profile, entry.Name(), "info.sii")
|
||||
if _, err := os.Stat(sFile); err != nil {
|
||||
// That directory contains no profile.sii - Weird but okay
|
||||
log.WithFields(log.Fields{
|
||||
"profile": profile,
|
||||
"save": entry.Name(),
|
||||
}).Debug("Found save directory without info.sii")
|
||||
continue
|
||||
}
|
||||
|
||||
unit, err := sii.ReadUnitFile(sFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to read unit for save %q", entry.Name())
|
||||
}
|
||||
|
||||
for _, b := range unit.Entries {
|
||||
if v, ok := b.(*sii.SaveContainer); ok {
|
||||
out[entry.Name()] = commSaveInfoFromSaveContainer(v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func listProfiles() (map[string]commProfileInfo, error) {
|
||||
entries, err := ioutil.ReadDir(getProfilesPath())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to list profiles")
|
||||
}
|
||||
|
||||
var out = map[string]commProfileInfo{}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
// There shouldn't be files but whatever
|
||||
continue
|
||||
}
|
||||
|
||||
var pFile = getProfileInfoPath(entry.Name())
|
||||
if _, err := os.Stat(pFile); err != nil {
|
||||
// That directory contains no profile.sii - Weird but okay
|
||||
log.WithField("profile", entry.Name()).Debug("Found profile directory without profile.sii")
|
||||
continue
|
||||
}
|
||||
|
||||
unit, err := sii.ReadUnitFile(pFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to read unit for profile %q", entry.Name())
|
||||
}
|
||||
|
||||
for _, b := range unit.Entries {
|
||||
if v, ok := b.(*sii.UserProfile); ok {
|
||||
out[entry.Name()] = commProfileInfoFromUserProfile(v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
246
cmd/sii-editor/saves.go
Normal file
246
cmd/sii-editor/saves.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/Luzifer/sii"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
fixAll = "all"
|
||||
fixCargoOnly = "cargo"
|
||||
fixTrailerOnly = "trailer"
|
||||
fixTruckOnly = "truck"
|
||||
)
|
||||
|
||||
type commSaveDetails struct {
|
||||
GameTime int64 `json:"game_time"`
|
||||
|
||||
ExperiencePoints int64 `json:"experience_points"`
|
||||
Money int64 `json:"money"`
|
||||
|
||||
CargoDamage float32 `json:"cargo_damage"`
|
||||
TruckWear float32 `json:"truck_wear"`
|
||||
TrailerAttached bool `json:"trailer_attached"`
|
||||
TrailerWear float32 `json:"trailer_wear"`
|
||||
|
||||
AttachedTrailer string `json:"attached_trailer"`
|
||||
OwnedTrailers map[string]string `json:"owned_trailers"`
|
||||
|
||||
CurrentJob *commSaveJob `json:"current_job"`
|
||||
}
|
||||
|
||||
type commSaveJob struct {
|
||||
OriginReference string `json:"origin_reference"`
|
||||
TargetReference string `json:"target_reference"`
|
||||
CargoReference string `json:"cargo_reference"`
|
||||
TrailerReference string `json:"trailer_reference"`
|
||||
Distance int64 `json:"distance"`
|
||||
Urgency *int64 `json:"urgency,omitempty"`
|
||||
Weight float32 `json:"weight,omitempty"`
|
||||
Expires int64 `json:"expires,omitempty"`
|
||||
}
|
||||
|
||||
func commSaveDetailsFromUnit(unit *sii.Unit) (out commSaveDetails, err error) {
|
||||
var economy *sii.Economy
|
||||
|
||||
for _, b := range unit.BlocksByClass("economy") {
|
||||
if v, ok := b.(*sii.Economy); ok {
|
||||
economy = v
|
||||
}
|
||||
}
|
||||
|
||||
if economy == nil {
|
||||
return out, errors.New("Found no economy object")
|
||||
}
|
||||
|
||||
var (
|
||||
bank *sii.Bank
|
||||
job *sii.PlayerJob
|
||||
player *sii.Player
|
||||
truck *sii.Vehicle
|
||||
trailer *sii.Trailer
|
||||
)
|
||||
|
||||
bank = economy.Bank.Resolve().(*sii.Bank)
|
||||
player = economy.Player.Resolve().(*sii.Player)
|
||||
|
||||
out.ExperiencePoints = economy.ExperiencePoints
|
||||
out.GameTime = economy.GameTime
|
||||
out.Money = bank.MoneyAccount
|
||||
out.TrailerAttached = player.AssignedTrailerConnected
|
||||
|
||||
if v, ok := player.CurrentJob.Resolve().(*sii.PlayerJob); ok {
|
||||
job = v
|
||||
}
|
||||
|
||||
if v, ok := player.AssignedTruck.Resolve().(*sii.Vehicle); ok {
|
||||
truck = v
|
||||
}
|
||||
|
||||
if v, ok := player.AssignedTrailer.Resolve().(*sii.Trailer); ok {
|
||||
out.AttachedTrailer = player.AssignedTrailer.Target
|
||||
trailer = v
|
||||
}
|
||||
|
||||
if truck != nil {
|
||||
for _, pb := range truck.Accessories {
|
||||
var wear float32
|
||||
if v, ok := pb.Resolve().(*sii.VehicleAccessory); ok {
|
||||
wear = v.Wear
|
||||
}
|
||||
|
||||
if v, ok := pb.Resolve().(*sii.VehicleWheelAccessory); ok {
|
||||
wear = v.Wear
|
||||
}
|
||||
|
||||
if wear > out.TruckWear {
|
||||
out.TruckWear = wear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(player.Trailers) > 0 {
|
||||
out.OwnedTrailers = map[string]string{}
|
||||
for _, tp := range player.Trailers {
|
||||
if t, ok := tp.Resolve().(*sii.Trailer); ok {
|
||||
out.OwnedTrailers[t.Name()] = t.CleanedLicensePlate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if trailer != nil {
|
||||
for _, pb := range trailer.Accessories {
|
||||
var wear float32
|
||||
if v, ok := pb.Resolve().(*sii.VehicleAccessory); ok {
|
||||
wear = v.Wear
|
||||
}
|
||||
|
||||
if v, ok := pb.Resolve().(*sii.VehicleWheelAccessory); ok {
|
||||
wear = v.Wear
|
||||
}
|
||||
|
||||
if wear > out.TrailerWear {
|
||||
out.TrailerWear = wear
|
||||
}
|
||||
}
|
||||
|
||||
out.CargoDamage = trailer.CargoDamage
|
||||
}
|
||||
|
||||
if job != nil {
|
||||
out.CurrentJob = &commSaveJob{
|
||||
OriginReference: job.SourceCompany.Target,
|
||||
TargetReference: job.TargetCompany.Target,
|
||||
CargoReference: job.Cargo.Target,
|
||||
TrailerReference: job.CompanyTrailer.Target,
|
||||
Distance: job.PlannedDistanceKM,
|
||||
Urgency: job.Urgency,
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func fixPlayerTruck(unit *sii.Unit, fixType string) error {
|
||||
// In call cases we need the player as the starting point
|
||||
var player *sii.Player
|
||||
|
||||
for _, b := range unit.Entries {
|
||||
if v, ok := b.(*sii.Player); ok {
|
||||
player = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if player == nil {
|
||||
return errors.New("Found no player object")
|
||||
}
|
||||
|
||||
// Fix truck
|
||||
if fixType == fixAll || fixType == fixTruckOnly {
|
||||
truck := player.AssignedTruck.Resolve().(*sii.Vehicle)
|
||||
for _, pb := range truck.Accessories {
|
||||
if v, ok := pb.Resolve().(*sii.VehicleAccessory); ok {
|
||||
v.Wear = 0
|
||||
}
|
||||
|
||||
if v, ok := pb.Resolve().(*sii.VehicleWheelAccessory); ok {
|
||||
v.Wear = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix trailer
|
||||
if (fixType == fixAll || fixType == fixTrailerOnly) && player.AssignedTrailer.Resolve() != nil {
|
||||
trailer := player.AssignedTrailer.Resolve().(*sii.Trailer)
|
||||
for _, pb := range trailer.Accessories {
|
||||
if v, ok := pb.Resolve().(*sii.VehicleAccessory); ok {
|
||||
v.Wear = 0
|
||||
}
|
||||
|
||||
if v, ok := pb.Resolve().(*sii.VehicleWheelAccessory); ok {
|
||||
v.Wear = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix cargo
|
||||
if (fixType == fixAll || fixType == fixCargoOnly) && player.AssignedTrailer.Resolve() != nil {
|
||||
trailer := player.AssignedTrailer.Resolve().(*sii.Trailer)
|
||||
trailer.CargoDamage = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSave reads game- and info- unit and returns them unmodified
|
||||
func loadSave(profile, save string) (*sii.Unit, *sii.SaveContainer, error) {
|
||||
var (
|
||||
saveInfoPath = getSaveFilePath(profile, save, "info.sii")
|
||||
saveUnitPath = getSaveFilePath(profile, save, "game.sii")
|
||||
)
|
||||
|
||||
infoUnit, err := sii.ReadUnitFile(saveInfoPath)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Unable to load save-info")
|
||||
}
|
||||
|
||||
var info *sii.SaveContainer
|
||||
for _, b := range infoUnit.Entries {
|
||||
if v, ok := b.(*sii.SaveContainer); ok {
|
||||
info = v
|
||||
}
|
||||
}
|
||||
|
||||
gameUnit, err := sii.ReadUnitFile(saveUnitPath)
|
||||
if err != nil {
|
||||
return nil, info, errors.Wrap(err, "Unable to load save-unit")
|
||||
}
|
||||
|
||||
return gameUnit, info, nil
|
||||
}
|
||||
|
||||
// storeSave writes game- and info- unit without checking for existance!
|
||||
func storeSave(profile, save string, unit *sii.Unit, info *sii.SaveContainer) error {
|
||||
var (
|
||||
saveInfoPath = getSaveFilePath(profile, save, "info.sii")
|
||||
saveUnitPath = getSaveFilePath(profile, save, "game.sii")
|
||||
)
|
||||
|
||||
if err := os.MkdirAll(path.Dir(saveInfoPath), 0700); err != nil {
|
||||
return errors.Wrap(err, "Unable to create save-dir")
|
||||
}
|
||||
|
||||
if err := sii.WriteUnitFile(saveInfoPath, &sii.Unit{Entries: []sii.Block{info}}); err != nil {
|
||||
return errors.Wrap(err, "Unable to write info unit")
|
||||
}
|
||||
|
||||
if err := sii.WriteUnitFile(saveUnitPath, unit); err != nil {
|
||||
return errors.Wrap(err, "Unable to write game unit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
118
t3nk/3nk.go
Normal file
118
t3nk/3nk.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package t3nk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// Key table entries were calculated from this formula:
|
||||
// Key[i] = (((i shl 2) xor not i) shl 3) xor i
|
||||
//
|
||||
// Or simpler: Stole it from https://github.com/idma88/3nK_Transcode/blob/master/Source/SII_3nK_Transcoder.pas
|
||||
keyTable = []byte{
|
||||
0xf8, 0xd1, 0xaa, 0x83, 0x5c, 0x75, 0x0e, 0x27, 0xb0, 0x99, 0xe2, 0xcb, 0x14, 0x3d, 0x46, 0x6f,
|
||||
0x68, 0x41, 0x3a, 0x13, 0xcc, 0xe5, 0x9e, 0xb7, 0x20, 0x09, 0x72, 0x5b, 0x84, 0xad, 0xd6, 0xff,
|
||||
0xd8, 0xf1, 0x8a, 0xa3, 0x7c, 0x55, 0x2e, 0x07, 0x90, 0xb9, 0xc2, 0xeb, 0x34, 0x1d, 0x66, 0x4f,
|
||||
0x48, 0x61, 0x1a, 0x33, 0xec, 0xc5, 0xbe, 0x97, 0x00, 0x29, 0x52, 0x7b, 0xa4, 0x8d, 0xf6, 0xdf,
|
||||
0xb8, 0x91, 0xea, 0xc3, 0x1c, 0x35, 0x4e, 0x67, 0xf0, 0xd9, 0xa2, 0x8b, 0x54, 0x7d, 0x06, 0x2f,
|
||||
0x28, 0x01, 0x7a, 0x53, 0x8c, 0xa5, 0xde, 0xf7, 0x60, 0x49, 0x32, 0x1b, 0xc4, 0xed, 0x96, 0xbf,
|
||||
0x98, 0xb1, 0xca, 0xe3, 0x3c, 0x15, 0x6e, 0x47, 0xd0, 0xf9, 0x82, 0xab, 0x74, 0x5d, 0x26, 0x0f,
|
||||
0x08, 0x21, 0x5a, 0x73, 0xac, 0x85, 0xfe, 0xd7, 0x40, 0x69, 0x12, 0x3b, 0xe4, 0xcd, 0xb6, 0x9f,
|
||||
0x78, 0x51, 0x2a, 0x03, 0xdc, 0xf5, 0x8e, 0xa7, 0x30, 0x19, 0x62, 0x4b, 0x94, 0xbd, 0xc6, 0xef,
|
||||
0xe8, 0xc1, 0xba, 0x93, 0x4c, 0x65, 0x1e, 0x37, 0xa0, 0x89, 0xf2, 0xdb, 0x04, 0x2d, 0x56, 0x7f,
|
||||
0x58, 0x71, 0x0a, 0x23, 0xfc, 0xd5, 0xae, 0x87, 0x10, 0x39, 0x42, 0x6b, 0xb4, 0x9d, 0xe6, 0xcf,
|
||||
0xc8, 0xe1, 0x9a, 0xb3, 0x6c, 0x45, 0x3e, 0x17, 0x80, 0xa9, 0xd2, 0xfb, 0x24, 0x0d, 0x76, 0x5f,
|
||||
0x38, 0x11, 0x6a, 0x43, 0x9c, 0xb5, 0xce, 0xe7, 0x70, 0x59, 0x22, 0x0b, 0xd4, 0xfd, 0x86, 0xaf,
|
||||
0xa8, 0x81, 0xfa, 0xd3, 0x0c, 0x25, 0x5e, 0x77, 0xe0, 0xc9, 0xb2, 0x9b, 0x44, 0x6d, 0x16, 0x3f,
|
||||
0x18, 0x31, 0x4a, 0x63, 0xbc, 0x95, 0xee, 0xc7, 0x50, 0x79, 0x02, 0x2b, 0xf4, 0xdd, 0xa6, 0x8f,
|
||||
0x88, 0xa1, 0xda, 0xf3, 0x2c, 0x05, 0x7e, 0x57, 0xc0, 0xe9, 0x92, 0xbb, 0x64, 0x4d, 0x36, 0x1f,
|
||||
}
|
||||
sii3nKSignature = uint32(0x014B6E33) // 3nK#01
|
||||
)
|
||||
|
||||
type sii3nKHeader struct {
|
||||
Signature uint32
|
||||
UnkByte uint8
|
||||
Seed uint8
|
||||
}
|
||||
|
||||
func Decode(r io.Reader) (io.Reader, error) {
|
||||
var hdr sii3nKHeader
|
||||
|
||||
if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to read header")
|
||||
}
|
||||
|
||||
if hdr.Signature != sii3nKSignature {
|
||||
return nil, errors.Errorf("Unexpected file signature: %d (expected %d)", hdr.Signature, sii3nKSignature)
|
||||
}
|
||||
|
||||
var (
|
||||
buf = new(bytes.Buffer)
|
||||
tBuf = make([]byte, 16*1024) // Buffer size = 16K
|
||||
)
|
||||
|
||||
for {
|
||||
n, err := r.Read(tBuf)
|
||||
if n > 0 {
|
||||
buf.Write(transcode(tBuf, n, hdr.Seed)[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, errors.Wrap(err, "Unable to read from input")
|
||||
}
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func Encode(r io.Reader) (io.Reader, error) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
var hdr = sii3nKHeader{
|
||||
Signature: sii3nKSignature,
|
||||
Seed: uint8(rand.Intn(256)),
|
||||
}
|
||||
|
||||
var (
|
||||
buf = new(bytes.Buffer)
|
||||
tBuf = make([]byte, 16*1024) // Buffer size = 16K
|
||||
)
|
||||
|
||||
if err := binary.Write(buf, binary.LittleEndian, hdr); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to write header")
|
||||
}
|
||||
|
||||
for {
|
||||
n, err := r.Read(tBuf)
|
||||
if n > 0 {
|
||||
buf.Write(transcode(tBuf, n, hdr.Seed)[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, errors.Wrap(err, "Unable to read from input")
|
||||
}
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func transcode(in []byte, size int, seed uint8) []byte {
|
||||
var out = make([]byte, len(in))
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
out[i] = in[i] ^ keyTable[byte(seed+uint8(i))]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
31
t3nk/3nk_test.go
Normal file
31
t3nk/3nk_test.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package t3nk
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodeToDecode(t *testing.T) {
|
||||
expect := "Ohai!"
|
||||
f := strings.NewReader(expect)
|
||||
|
||||
r, err := Encode(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to encode test string: %s", err)
|
||||
}
|
||||
|
||||
dr, err := Decode(r)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to decode test string: %s", err)
|
||||
}
|
||||
|
||||
raw, err := ioutil.ReadAll(dr)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read decoded test string: %s", err)
|
||||
}
|
||||
|
||||
if s := string(raw); s != expect {
|
||||
t.Errorf("Did not receive expected string: exp=%q got=%q", expect, s)
|
||||
}
|
||||
}
|
5
t3nk/go.mod
Normal file
5
t3nk/go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module github.com/Luzifer/sii/t3nk
|
||||
|
||||
go 1.13
|
||||
|
||||
require github.com/pkg/errors v0.8.1
|
2
t3nk/go.sum
Normal file
2
t3nk/go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
Loading…
Reference in a new issue