Files
re4-biorand-reseed/main.go
2024-11-28 23:38:39 +01:00

376 lines
10 KiB
Go

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