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.
This commit is contained in:
Houmgaor
2026-03-23 19:55:55 +01:00
parent 9e59ce69e3
commit 6f7852cc12
4 changed files with 78 additions and 0 deletions

View File

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

View File

@@ -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

View File

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

View File

@@ -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
}