initial commit
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
reseed-config.json
|
||||||
|
biorand-seeds/
|
||||||
|
.vscode/launch.json
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/mtfarkas/re4-biorand-reseed
|
||||||
|
|
||||||
|
go 1.23.3
|
||||||
11
json_ex/json.go
Normal file
11
json_ex/json.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package json_ex
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
func GenericUnmarshal[T any](source []byte) (T, error) {
|
||||||
|
var target T
|
||||||
|
if err := json.Unmarshal(source, &target); err != nil {
|
||||||
|
return target, err
|
||||||
|
}
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
375
main.go
Normal file
375
main.go
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mtfarkas/re4-biorand-reseed/json_ex"
|
||||||
|
)
|
||||||
|
|
||||||
|
var seedRunes = []rune("0123456789")
|
||||||
|
|
||||||
|
const SEPARATOR = "================================"
|
||||||
|
const RANDO_PROFILE_ID = 7 // 7rayD's Balanced Combat Randomizer
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
RE4InstallPath string
|
||||||
|
BiorandToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BiorandProfile struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ConfigId int `json:"configId"`
|
||||||
|
Config map[string]interface{} `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateRequest struct {
|
||||||
|
ProfileID int `json:"profileId"`
|
||||||
|
Seed string `json:"seed"`
|
||||||
|
Config map[string]interface{} `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateResponse struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryGenerationResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
DownloadUrl string `json:"downloadUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthenticatedHttpRequest(url string, method string, token string, body io.Reader) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating HTTP request; %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSeed() string {
|
||||||
|
b := make([]rune, 6)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = seedRunes[rand.Intn(len(seedRunes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfiguration() (*Config, error) {
|
||||||
|
jsonFile, err := os.Open("reseed-config.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading config file; %w", err)
|
||||||
|
}
|
||||||
|
defer jsonFile.Close()
|
||||||
|
allBytes, _ := io.ReadAll(jsonFile)
|
||||||
|
|
||||||
|
config, err := json_ex.GenericUnmarshal[Config](allBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling config file; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.BiorandToken) < 1 {
|
||||||
|
return nil, fmt.Errorf("the Biorand token can't be empty")
|
||||||
|
}
|
||||||
|
if len(config.RE4InstallPath) < 1 {
|
||||||
|
return nil, fmt.Errorf("RE4 install path can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBiorandProfileConfiguration(profileId int, biorandToken string) (*BiorandProfile, error) {
|
||||||
|
client := http.Client{}
|
||||||
|
|
||||||
|
req, err := getAuthenticatedHttpRequest("https://api-re4r.biorand.net/profile", "GET", biorandToken, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating Profile request; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error calling Profile API; %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading Profile API response; %w", err)
|
||||||
|
}
|
||||||
|
profiles, err := json_ex.GenericUnmarshal[[]BiorandProfile](bodyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling Profile API response; %w", err)
|
||||||
|
}
|
||||||
|
idx := slices.IndexFunc(profiles, func(p BiorandProfile) bool { return p.ID == profileId })
|
||||||
|
if idx < 0 {
|
||||||
|
return nil, fmt.Errorf("couldn't find profile with ID %d in Profile API response. Make sure you bookmark it with your account", profileId)
|
||||||
|
}
|
||||||
|
return &profiles[idx], nil
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("response from Profile API doesn't indicate success; %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateBiorandSeed(seed string, profile *BiorandProfile, biorandToken string) (*GenerateResponse, error) {
|
||||||
|
client := http.Client{}
|
||||||
|
|
||||||
|
reqBody := GenerateRequest{
|
||||||
|
ProfileID: profile.ID,
|
||||||
|
Seed: seed,
|
||||||
|
Config: profile.Config,
|
||||||
|
}
|
||||||
|
reqBodyBytes, _ := json.Marshal(reqBody)
|
||||||
|
|
||||||
|
req, err := getAuthenticatedHttpRequest("https://api-re4r.biorand.net/rando/generate", "POST", biorandToken, bytes.NewBuffer(reqBodyBytes))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating Generate request; %w", err)
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error calling Generate API; %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("response from Generate API doesn't indicate success; %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
generateResponseBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
generateResponse, err := json_ex.GenericUnmarshal[GenerateResponse](generateResponseBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling Generate API response; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &generateResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryBiorandSeedDownloadLink(generateStartResponse *GenerateResponse, biorandToken string) (*QueryGenerationResponse, error) {
|
||||||
|
client := http.Client{}
|
||||||
|
req, err := getAuthenticatedHttpRequest(fmt.Sprintf("https://api-re4r.biorand.net/rando/%d", generateStartResponse.ID), "GET", biorandToken, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating Query Generate request; %w", err)
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error calling Query Generate API; %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("response from Query Generate API doesn't indicate success; %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
queryResponseBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
queryResponse, err := json_ex.GenericUnmarshal[QueryGenerationResponse](queryResponseBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling Query Generate API response; %w", err)
|
||||||
|
}
|
||||||
|
return &queryResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadSeedZip(seed string, downloadUrl string) (string, error) {
|
||||||
|
client := http.Client{}
|
||||||
|
|
||||||
|
resp, err := client.Get(downloadUrl)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting seed zip response; %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("download file response doesn't indicate success: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("biorand-re4r-%s.zip", seed)
|
||||||
|
contentDispositionHeader := resp.Header.Get("Content-Disposition")
|
||||||
|
if len(contentDispositionHeader) > 0 {
|
||||||
|
_, params, err := mime.ParseMediaType(contentDispositionHeader)
|
||||||
|
if err == nil {
|
||||||
|
fileName = params["filename"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadDir := path.Join("biorand-seeds", seed)
|
||||||
|
err = os.MkdirAll(downloadDir, os.FileMode(int(0777)))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create biorand seed folder; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullFilePath := path.Join(downloadDir, fileName)
|
||||||
|
|
||||||
|
out, err := os.Create(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error creating file to download to; %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error saving seed file; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unzipArchiveToDestination(zipFile string, dest string) error {
|
||||||
|
r, err := zip.OpenReader(zipFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error opening zip file %s; %w", zipFile, err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
fmt.Printf("Unzipping %s...\n", f.Name)
|
||||||
|
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading file from zip; %w", err)
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
fpath := filepath.Join(dest, f.Name)
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(fpath, f.Mode())
|
||||||
|
} else {
|
||||||
|
var fdir string
|
||||||
|
if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 {
|
||||||
|
fdir = fpath[:lastIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(fdir, f.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating directories while unzipping; %w", err)
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(
|
||||||
|
fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(f, rc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error writing unzipped file; %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Generating new seed...")
|
||||||
|
|
||||||
|
config, err := getConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error getting configuration: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("RE4 path: %s\n", config.RE4InstallPath)
|
||||||
|
fmt.Printf("Profile ID: %d\n", RANDO_PROFILE_ID)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("Getting randomizer profile...")
|
||||||
|
profile, err := getBiorandProfileConfiguration(RANDO_PROFILE_ID, config.BiorandToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error getting profile information: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Profile info downloaded.")
|
||||||
|
fmt.Printf("Profile name: %s\n", profile.Name)
|
||||||
|
fmt.Printf("Profile description: %s\n", profile.Description)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
seed := generateSeed()
|
||||||
|
fmt.Printf("Generated the following seed: %s", seed)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("Continue? (y/n)")
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
text, _ := reader.ReadString('\n')
|
||||||
|
if !strings.EqualFold(strings.Trim(text, "\r\n"), "y") {
|
||||||
|
fmt.Println("Reseeding aborted.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Generating new seed on Biorand...")
|
||||||
|
seedResponse, err := generateBiorandSeed(seed, profile, config.BiorandToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error generating Biorand seed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadLink := ""
|
||||||
|
errorCount := 0
|
||||||
|
attemptCount := 0
|
||||||
|
for {
|
||||||
|
attemptCount += 1
|
||||||
|
|
||||||
|
if attemptCount >= 180 {
|
||||||
|
fmt.Println("Seed generation timed out. Aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryResponse, err := queryBiorandSeedDownloadLink(seedResponse, config.BiorandToken)
|
||||||
|
if err != nil {
|
||||||
|
errorCount += 1
|
||||||
|
fmt.Printf("Error querying Biorand API (%d/3): %v\n", errorCount, err)
|
||||||
|
|
||||||
|
if errorCount > 3 {
|
||||||
|
fmt.Printf("Error count treshold reached. Aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorCount = 0
|
||||||
|
|
||||||
|
if queryResponse.Status == 1 {
|
||||||
|
fmt.Println("Seed is queued for generation.")
|
||||||
|
} else if queryResponse.Status == 2 {
|
||||||
|
fmt.Println("Seed is being generated.")
|
||||||
|
} else if queryResponse.Status == 3 {
|
||||||
|
fmt.Println("Seed is done generating.")
|
||||||
|
downloadLink = queryResponse.DownloadUrl
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
fmt.Println("Seed status unknown; Aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Downloading seed zip...")
|
||||||
|
zipPath, err := downloadSeedZip(seed, downloadLink)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error downloading seed zip: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Seed zip successfully downloaded to %s\n", zipPath)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Printf("Unzipping seed zip to %s...\n", config.RE4InstallPath)
|
||||||
|
err = unzipArchiveToDestination(zipPath, config.RE4InstallPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to unzip seed: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Reseeding completed. Enjoy!")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
4
reseed-config-sample.json
Normal file
4
reseed-config-sample.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"BiorandToken": "",
|
||||||
|
"RE4InstallPath": ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user