package api import ( "fmt" "net/http" "net/url" "strings" ) // LauncherAPIHost is the official JP launcher API host observed from the PC launcher. const LauncherAPIHost = "api-launcher-jp.yo-star.com" func (s *Server) handleLauncherBootstrap(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed") return } sum := s.releaseSummary() status := http.StatusOK if !sum.Ready || sum.Snapshot == nil { status = http.StatusServiceUnavailable } writeNoStoreJSON(w, status, s.launcherBootstrapBody(sum)) } func (s *Server) handleLauncherGameConfig(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed") return } sum := s.releaseSummary() if !sum.Ready || sum.Snapshot == nil { writeLauncherEnvelope(w, http.StatusServiceUnavailable, 503, "release not ready", LauncherGameConfigJSONData{ ResourceBootstrapURL: s.launcherBootstrapURL(), Scope: "resource_bootstrap_only", }) return } snap := sum.Snapshot data := LauncherGameConfigData{ GameLatestVersion: defaultString(s.launcherLatestVersion(snap), snap.AppVersion), GameLatestFilePath: s.launcherLatestFilePath(snap), GameStartExeName: s.launcherStartExeName(snap), GameStartParams: s.launcherStartParams(snap), ResourceBootstrapURL: s.launcherBootstrapURL(), ServerInfoURL: s.serverInfoURL(), ClientPatchBaseURL: s.clientPatchBaseURL(), Scope: "resource_bootstrap_only", } if snap.LauncherMetadata != nil && snap.LauncherMetadata.GameLowestVersion != "" { data.GameLowestVersion = snap.LauncherMetadata.GameLowestVersion } writeLauncherEnvelope(w, http.StatusOK, 200, "ok", data) } func (s *Server) handleLauncherGameConfigJSON(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed") return } sum := s.releaseSummary() if !sum.Ready || sum.Snapshot == nil { writeLauncherEnvelope(w, http.StatusServiceUnavailable, 503, "release not ready", LauncherGameConfigJSONData{ URL: s.launcherBootstrapURL(), ResourceBootstrapURL: s.launcherBootstrapURL(), Scope: "resource_bootstrap_only", }) return } writeLauncherEnvelope(w, http.StatusOK, 200, "ok", LauncherGameConfigJSONData{ URL: s.launcherBootstrapJSONURL(r.URL.Query()), ResourceBootstrapURL: s.launcherBootstrapURL(), PackageUpdateManifest: false, Scope: "resource_bootstrap_only", }) } func (s *Server) handleLauncherCdnConfig(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed") return } writeLauncherEnvelope(w, http.StatusOK, 200, "ok", LauncherCdnConfigData{ PrimaryCDN: strings.TrimRight(s.cfg.PublicBaseURL, "/"), BackupCDN: strings.TrimRight(s.cfg.PublicBaseURL, "/"), ResourceBootstrapURL: s.launcherBootstrapURL(), PackageUpdateManifest: false, Scope: "resource_bootstrap_only", }) } func (s *Server) launcherBootstrapBody(sum ReleaseSummary) LauncherBootstrapResponse { publicBase := strings.TrimRight(s.cfg.PublicBaseURL, "/") body := LauncherBootstrapResponse{ Service: "bat-api", Ready: sum.Ready, LauncherAPI: LauncherAPIInfo{ Host: LauncherAPIHost, ObservedResourceRelevantEndpoints: []string{ "/api/launcher/game/config", "/api/launcher/game/config/json", "/api/launcher/advanced/game/download/cdn", }, CompatibilityScope: "resource_bootstrap_only", }, Policy: LauncherPolicy{ Source: "rust_bat_snapshot", DownloadsLauncherPackage: false, EmulatesLoginOrGateway: false, PackageUpdateManifest: false, }, Resource: LauncherResource{ Release: sum.Snapshot, ServerInfoURL: s.serverInfoURL(), ClientPatchBaseURL: s.clientPatchBaseURL(), }, Endpoints: LauncherEndpointSet{ Bootstrap: publicBase + "/v1/bootstrap", LauncherBootstrap: publicBase + "/v1/launcher/bootstrap", ServerInfo: s.serverInfoURL(), ClientPatchBase: s.clientPatchBaseURL(), LauncherGameConfig: publicBase + "/" + LauncherAPIHost + "/api/launcher/game/config", LauncherGameConfigJSON: publicBase + "/" + LauncherAPIHost + "/api/launcher/game/config/json", LauncherCdnConfig: publicBase + "/" + LauncherAPIHost + "/api/launcher/advanced/game/download/cdn", }, } if sum.Snapshot != nil { body.GameMainConfig = sum.Snapshot.GameMainConfig body.LauncherMetadata = sum.Snapshot.LauncherMetadata if rewritten, ok := rewriteAddressablesRoot(sum.Snapshot.AddressablesRoot, publicBase); ok { body.AddressablesCatalogURLRoot = rewritten } } return body } func (s *Server) releaseSummary() ReleaseSummary { if idx := s.index(); idx != nil { return idx.Summary() } return ReleaseSummary{} } func (s *Server) serverInfoURL() string { return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + ServerInfoHost + "/server-info.json" } func (s *Server) clientPatchBaseURL() string { return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + ClientPatchHost } func (s *Server) launcherBootstrapURL() string { return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/v1/launcher/bootstrap" } func (s *Server) launcherBootstrapJSONURL(query url.Values) string { base := strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + LauncherAPIHost + "/api/launcher/resource/bootstrap.json" values := url.Values{} for _, key := range []string{"version", "file_path"} { if value := query.Get(key); value != "" { values.Set(key, value) } } if encoded := values.Encode(); encoded != "" { return base + "?" + encoded } return base } func (s *Server) launcherLatestVersion(snap *SnapshotSummary) string { if snap.LauncherMetadata != nil && snap.LauncherMetadata.GameLatestVersion != "" { return snap.LauncherMetadata.GameLatestVersion } return snap.AppVersion } func (s *Server) launcherLatestFilePath(snap *SnapshotSummary) string { if snap.LauncherMetadata != nil { return snap.LauncherMetadata.GameLatestFilePath } return "" } func (s *Server) launcherStartExeName(snap *SnapshotSummary) string { if snap.LauncherMetadata != nil { return snap.LauncherMetadata.GameStartExeName } return "" } func (s *Server) launcherStartParams(snap *SnapshotSummary) []string { if snap.LauncherMetadata != nil && len(snap.LauncherMetadata.GameStartParams) > 0 { return append([]string(nil), snap.LauncherMetadata.GameStartParams...) } return []string{} } func writeLauncherEnvelope[T any](w http.ResponseWriter, status int, code int, message string, data T) { writeNoStoreJSON(w, status, LauncherEnvelope[T]{ Code: code, Message: message, Data: data, }) } func launcherHostPath(path string) string { return fmt.Sprintf("/%s%s", LauncherAPIHost, path) }