1
0
Fork 0
mirror of https://github.com/Luzifer/sii.git synced 2024-10-18 05:14:19 +00:00

Implement Save-Game Editor (#1)

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2019-12-28 13:06:28 +00:00 committed by GitHub
parent 57e2be2495
commit ab39609f45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 2415 additions and 0 deletions

1
cmd/sii-editor/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

7
cmd/sii-editor/Makefile Normal file
View 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
View 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)
}

View 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)
}

View 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
View 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
View 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)
}

View 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[*]}"

View 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
View 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")
}

View 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")
}

View 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
View file

@ -0,0 +1,3 @@
combine.css
combine.js
fontawesome

View 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; }

View 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()
},
},
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.9 KiB

View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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",
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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=