From 6f7852cc12ceb6611ab0b49516728023c455ca30 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 23 Mar 2026 19:55:55 +0100 Subject: [PATCH] feat(api): add GET /v2/server/info endpoint for launcher compatibility Exposes the server's configured client mode in a form that mhf-outpost and other launcher tools can consume to warn users when their local game version does not match what the server expects. Returns clientMode (raw, e.g. "G9.1") and manifestId (normalized for mhf-outpost: lowercase, dots stripped, e.g. "g91"). No auth required. --- server/api/api_server.go | 1 + server/api/endpoints.go | 26 ++++++++++++++ server/api/endpoints_coverage_test.go | 50 +++++++++++++++++++++++++++ server/api/routing_test.go | 1 + 4 files changed, 78 insertions(+) diff --git a/server/api/api_server.go b/server/api/api_server.go index 68b4aedc7..5f03f0ac4 100644 --- a/server/api/api_server.go +++ b/server/api/api_server.go @@ -86,6 +86,7 @@ func (s *APIServer) Start() error { v2.HandleFunc("/version", s.Version).Methods("GET") v2.HandleFunc("/health", s.Health).Methods("GET") v2.HandleFunc("/server/status", s.ServerStatus).Methods("GET") + v2.HandleFunc("/server/info", s.ServerInfo).Methods("GET") // V2 authenticated routes v2Auth := v2.PathPrefix("").Subrouter() diff --git a/server/api/endpoints.go b/server/api/endpoints.go index c1519ad9c..29c4ae71e 100644 --- a/server/api/endpoints.go +++ b/server/api/endpoints.go @@ -156,6 +156,32 @@ func (s *APIServer) Version(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(resp) } +// ServerInfoResponse is the JSON payload returned by GET /v2/server/info. +// It exposes the server's configured game version in a form that launcher +// tools (e.g. mhf-outpost) can use to check version compatibility. +type ServerInfoResponse struct { + // ClientMode is the version string as configured in Erupe (e.g. "ZZ", "G10.1"). + ClientMode string `json:"clientMode"` + // ManifestID is the normalized form of ClientMode (lowercase, dots removed) + // matching mhf-outpost manifest IDs (e.g. "zz", "g101"). + ManifestID string `json:"manifestId"` + // Name is the server software name. + Name string `json:"name"` +} + +// ServerInfo handles GET /v2/server/info, returning the server's configured +// game version in a format compatible with mhf-outpost manifest IDs. +func (s *APIServer) ServerInfo(w http.ResponseWriter, r *http.Request) { + clientMode := s.erupeConfig.ClientMode + resp := ServerInfoResponse{ + ClientMode: clientMode, + ManifestID: strings.ToLower(strings.ReplaceAll(clientMode, ".", "")), + Name: "Erupe-CE", + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + // Launcher handles GET /launcher and returns banners, messages, and links for the launcher UI. func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) { var respData LauncherResponse diff --git a/server/api/endpoints_coverage_test.go b/server/api/endpoints_coverage_test.go index 9eaa26e7a..4c520faa4 100644 --- a/server/api/endpoints_coverage_test.go +++ b/server/api/endpoints_coverage_test.go @@ -44,6 +44,56 @@ func TestVersionEndpoint(t *testing.T) { } } +func TestServerInfoEndpoint(t *testing.T) { + tests := []struct { + clientMode string + wantID string + }{ + {"ZZ", "zz"}, + {"GG", "gg"}, + {"G10.1", "g101"}, + {"G9.1", "g91"}, + {"FW.5", "fw5"}, + } + for _, tt := range tests { + t.Run(tt.clientMode, func(t *testing.T) { + logger := NewTestLogger(t) + c := NewTestConfig() + c.ClientMode = tt.clientMode + + server := &APIServer{ + logger: logger, + erupeConfig: c, + } + + req := httptest.NewRequest("GET", "/v2/server/info", nil) + rec := httptest.NewRecorder() + server.ServerInfo(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("status = %d, want 200", rec.Code) + } + if ct := rec.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("Content-Type = %q, want application/json", ct) + } + + var resp ServerInfoResponse + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("decode error: %v", err) + } + if resp.ClientMode != tt.clientMode { + t.Errorf("ClientMode = %q, want %q", resp.ClientMode, tt.clientMode) + } + if resp.ManifestID != tt.wantID { + t.Errorf("ManifestID = %q, want %q", resp.ManifestID, tt.wantID) + } + if resp.Name != "Erupe-CE" { + t.Errorf("Name = %q, want Erupe-CE", resp.Name) + } + }) + } +} + func TestLandingPageEndpoint_Enabled(t *testing.T) { logger := NewTestLogger(t) c := NewTestConfig() diff --git a/server/api/routing_test.go b/server/api/routing_test.go index d72f7567b..a681f476c 100644 --- a/server/api/routing_test.go +++ b/server/api/routing_test.go @@ -44,6 +44,7 @@ func newTestRouter(s *APIServer) *mux.Router { v2Auth.HandleFunc("/characters/{id}/export", s.ExportSave).Methods("GET") v2.HandleFunc("/server/status", s.ServerStatus).Methods("GET") + v2.HandleFunc("/server/info", s.ServerInfo).Methods("GET") return r }