Add configuration
17 files changed, 216 insertions(+), 76 deletions(-) M .gitignore M README.md M cmd/gemif/main.go M go.mod M go.sum A pkg/gamemanager/engineconfig.go M pkg/gamemanager/gamemanager.go M pkg/gamemanager/statetoken.pb.go M pkg/gamemanager/statetoken.proto R pkg/gamemanager/{config.go => storybook.go} R pkg/{web/banners.go => gemserver/banners.go} R pkg/{web/router.go => gemserver/router.go} R pkg/{web/routes.go => gemserver/routes.go} A pkg/gemserver/serverconfig.go R pkg/{web/template.go => gemserver/template.go} A sample.config.toml M static/docs/api.gmi
M .gitignore => .gitignore +2 -1
@@ 3,4 3,5 @@ my.key pkged.go tmp/ bin/ coverage.out \ No newline at end of file coverage.out config.toml
M README.md => README.md +40 -18
@@ 14,19 14,41 @@ There's no simple installation process at the moment. For now, you'll need to clone this repo and build or run `cmd/gemif` with something like `make build-all`, `go build gemif/cmd/gemif`, or whatever. Then run the compiled binary with the cloned repo as your working directory. ### Configure You'll also need to generate or copy in your TLS cert to `my.crt` and `my.key`. Once you have the binary compiled, you need a few files to make it run. The templates and static assets are compiled into the binary and cannot be changed at runtime, but the `file.yml` holding your story and cert/private key are read directly from your current working directory. #### Step One: Generate your TLS Cert ## Managing Your Story The first thing to do is generate your TLS cert in whatever way makes sense for you. Stories must live in the `./stories` directory in your working directory. At least for right now, maybe it'll be configurable later. In production, you might use [certbot](https://certbot.eff.org/docs/using.html#standalone) in standalone mode to generate a LetsEncrypt cert. Otherwise, since most (all?) Gemini clients use TOFU for TLS certificates, you can also feel free to [generate a self-signed cer](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs) in whatever way is appropriate for you. #### Step Two: Gather up your stories This repo comes with a couple of sample stories in `./stories`. If you want to add, remove, or change them, go ahead and do that now. #### Step Three: Make a config file GemIF configures itself based on the contents of `./config.toml`. Take `sample.config.toml` and rename it. Change the configuration to match your desired values. ### Run it That's it. Go ahead and run the binary. ## Managing Your Stories By default, stories live in the `./stories` directory in your working directory. You can change this with `engine.stories_dir` in your `config.toml`. Each `*.yml` file in that directory represents a separate story to be loaded @@ up by the engine. The top-level index page will allow the user to choose which 44,17 66,17 @@ The file must contain two top-level properties: `metadata` and `rooms`. The `rooms` object holds a list of your rooms/scenes. Each room has: * room_id: a unique id for the room, I recommend a GUID * room_name: a human-readable name * room_description: a description of the room (what is printed to the screen when the user is there) * exits: an array of exit objects * `room_id`: a unique id for the room, I recommend a GUID * `room_name`: a human-readable name * `room_description`: a description of the room (what is printed to the screen when the user is there) * `exits`: an array of exit objects Each exit contains: * exit_description: a description of the exit (the text of the exit link) * destiantion_id: the id of the room this exit takes you to * exit_id: a unique id for the exit, I recommend a GUID * set_condition: conditional tag to attach to the game state if the user takes this exit * if_condition: conditional tag the user must posses to use this exit * `exit_description`: a description of the exit (the text of the exit link) * `desination_id`: the id of the room this exit takes you to * `exit_id`: a unique id for the exit, I recommend a GUID * `set_condition`: conditional tag to attach to the game state if the user takes this exit * `if_condition`: conditional tag the user must posses to use this exit ### Conditions
M cmd/gemif/main.go => cmd/gemif/main.go +33 -9
@@ 4,8 4,11 @@ package main import ( "fmt" "gemif/pkg/gamemanager" "gemif/pkg/web" "os" "gemif/pkg/gemserver" "io/ioutil" "log" "github.com/pelletier/go-toml" ) @@ //nolint:gochecknoglobals 17,18 20,39 @@ var ( ) func main() { config, loaderErr := gamemanager.NewConfig("./stories") if loaderErr != nil { fmt.Printf("Error loading stories! %s", loaderErr) file, err := ioutil.ReadFile("./config.toml") if err != nil { log.Fatalf("Couldn't load config file: %s", err) } config, err := toml.Load(string(file)) if err != nil { log.Fatalf("Could not parse config file: %s", err) } gameMgr, err := gamemanager.NewGameManager(config) book, err := gamemanager.NewStoryBook(config.Get("engine.stories_dir").(string)) if err != nil { fmt.Printf("Error loading game! %s", err) os.Exit(1) log.Fatalf("Error loading stories! %s", err) } renderDesc := config.Get("engine.render_descriptions").(bool) tokenFormat := config.Get("engine.statetoken_format").(string) engineConfig := gamemanager.NewEngineConfig(renderDesc, tokenFormat) gameMgr, err := gamemanager.NewGameManager(book, engineConfig) if err != nil { log.Fatalf("Error loading game! %s", err) } gsConfig := config.Get("gemserver").(*toml.Tree) serverConfig := gemserver.ServerConfig{ Domain: gsConfig.Get("domain").(string), Port: gsConfig.Get("port").(int64), CertFile: gsConfig.Get("cert_file").(string), KeyFile: gsConfig.Get("key_file").(string), } fmt.Printf("Starting %s %s (%s) - built %s\n\n", appName, appVersion, appCommit, buildTime) web.StartRouter(gameMgr) gemserver.StartRouter(gameMgr, serverConfig) }
M go.mod => go.mod +7 -2
@@ 3,9 3,14 @@ module gemif go 1.14 require ( github.com/google/go-cmp v0.5.2 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/markbates/pkger v0.17.1 github.com/pelletier/go-toml v1.8.1 github.com/pitr/gig v0.9.7 github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.6.1 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/protobuf v1.25.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 )
M go.sum => go.sum +16 -2
@@ 26,8 26,12 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 36,6 40,8 @@ github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCn github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matryer/is v1.3.0 h1:9qiso3jaJrOe6qBRJRBt2Ldht05qDiFP9le0JOIhRSI= github.com/matryer/is v1.3.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pitr/gig v0.9.7 h1:VMwGTynX2LHppwOJTlEeUtIZnPOG/iwOMWvDZ61/RXQ= github.com/pitr/gig v0.9.7/go.mod h1:YHUShtPtgG/zAsdlVG2HyzfGA1EKB+QBVFKxJ2qzxhU= @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45,6 51,8 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= 71,6 79,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 92,9 102,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
A pkg/gamemanager/engineconfig.go => pkg/gamemanager/engineconfig.go +15 -0
@@ 0,0 1,15 @@ package gamemanager // EngineConfig contains settings information for driving the engine. type EngineConfig struct { RenderDescriptions bool StateTokenFormat string } // NewEngineConfig news up an EngineConfig based on passed in values. func NewEngineConfig(renderDesc bool, tokenFormat string) EngineConfig { return EngineConfig{ RenderDescriptions: renderDesc, StateTokenFormat: tokenFormat, } }
M pkg/gamemanager/gamemanager.go => pkg/gamemanager/gamemanager.go +38 -13
@@ 5,12 5,14 @@ import ( "errors" "fmt" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) // GameManager contains configuration info about stories and managed GameState objects. type GameManager struct { Config Config Book StoryBook EngineConfig EngineConfig } @@ // SpeculativeState is used to provide "what if" GameStates for next available actions. 20,6 22,8 @@ type SpeculativeState struct { } var ( // ErrInvalidSerializationFormat occurs when engine config specifies a unsupported format. ErrInvalidSerializationFormat = errors.New("invalid serialization format") // ErrRoomNotFound occurs when the room doesn't exist in the given story. ErrRoomNotFound = errors.New("room not found") @@ // ErrExitNotFound occurs when the exit doesn't exit in the given room. 27,26 31,29 @@ var ( ) // NewGameManager creates a new GameManager, using Stories in the Config. func NewGameManager(config Config) (*GameManager, error) { return &GameManager{config}, nil func NewGameManager(book StoryBook, config EngineConfig) (*GameManager, error) { return &GameManager{ Book: book, EngineConfig: config, }, nil } // StartGame creates a new GameState representing the start of a given storyID. func (gm *GameManager) StartGame(storyID string) (GameState, error) { return GameState{ StoryID: storyID, CurrentRoom: gm.Config.Stories[storyID].Rooms[0].ID, CurrentRoom: gm.Book.Stories[storyID].Rooms[0].ID, Conditions: []string{}, }, nil } // GetStories lists all StoryMetadata for stories tracked by GameManager. func (gm *GameManager) GetStories() []StoryMetadata { metadata := make([]StoryMetadata, len(gm.Config.Stories)) metadata := make([]StoryMetadata, len(gm.Book.Stories)) i := 0 for _, e := range gm.Config.Stories { for _, e := range gm.Book.Stories { metadata[i] = e.Metadata i++ @@ } 56,7 63,7 @@ func (gm *GameManager) GetStories() []StoryMetadata { // GetRoomByID finds Room information based on room and story IDs. func (gm *GameManager) GetRoomByID(storyID string, roomID string) (Room, error) { for _, r := range gm.Config.Stories[storyID].Rooms { for _, r := range gm.Book.Stories[storyID].Rooms { if r.ID == roomID { return r, nil @@ } 101,8 108,15 @@ func (gm *GameManager) DeserializeState(stateToken string) (GameState, error) { } var st StateToken if unmarshallErr := proto.Unmarshal(decodedString, &st); unmarshallErr != nil { return GameState{}, fmt.Errorf("failed to load state: %w", unmarshallErr) if gm.EngineConfig.StateTokenFormat == "proto" { if unmarshallErr := proto.Unmarshal(decodedString, &st); unmarshallErr != nil { return GameState{}, fmt.Errorf("failed to load state: %w", unmarshallErr) } } else if gm.EngineConfig.StateTokenFormat == "json" { if unmarshallErr := protojson.Unmarshal(decodedString, &st); unmarshallErr != nil { return GameState{}, fmt.Errorf("failed to load state: %w", unmarshallErr) } } @@ return GameState{ 120,12 134,23 @@ func (gm *GameManager) SerializeState(gameState GameState) (string, error) { Conditions: gameState.Conditions, } protoBytes, marshallErr := proto.Marshal(&st) if marshallErr != nil { return "", fmt.Errorf("failed to save state: %w", marshallErr) if gm.EngineConfig.StateTokenFormat == "proto" { stateBytes, err := proto.Marshal(&st) if err != nil { return "", fmt.Errorf("failed to save state: %w", err) } return base64.StdEncoding.EncodeToString(stateBytes), nil } else if gm.EngineConfig.StateTokenFormat == "json" { stateBytes, err := protojson.Marshal(&st) if err != nil { return "", fmt.Errorf("failed to save state: %w", err) } return base64.StdEncoding.EncodeToString(stateBytes), nil } return base64.StdEncoding.EncodeToString(protoBytes), nil return "", fmt.Errorf("%w: %s", ErrInvalidSerializationFormat, gm.EngineConfig.StateTokenFormat) } // ConstructSpeculativeStates makes a SpeculativeState from the active GameState and actions available in currentRoom.
M pkg/gamemanager/statetoken.pb.go => pkg/gamemanager/statetoken.pb.go +11 -12
@@ 25,9 25,9 @@ type StateToken struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields StoryID string `protobuf:"bytes,1,opt,name=StoryID,proto3" json:"StoryID,omitempty"` CurrentRoom string `protobuf:"bytes,2,opt,name=CurrentRoom,proto3" json:"CurrentRoom,omitempty"` Conditions []string `protobuf:"bytes,3,rep,name=Conditions,proto3" json:"Conditions,omitempty"` StoryID string `protobuf:"bytes,1,opt,name=StoryID,json=sid,proto3" json:"StoryID,omitempty"` CurrentRoom string `protobuf:"bytes,2,opt,name=CurrentRoom,json=cr,proto3" json:"CurrentRoom,omitempty"` Conditions []string `protobuf:"bytes,3,rep,name=Conditions,json=cn,proto3" json:"Conditions,omitempty"` } @@ func (x *StateToken) Reset() { 88,15 88,14 @@ var File_gamemanager_statetoken_proto protoreflect.FileDescriptor var file_gamemanager_statetoken_proto_rawDesc = []byte{ 0x0a, 0x1c, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x22, 0x68, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x0d, 0x5a, 0x0b, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x22, 0x53, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x07, 0x53, 0x74, 0x6f, 0x72, 0x79, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x69, 0x64, 0x12, 0x17, 0x0a, 0x0b, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x63, 0x72, 0x12, 0x16, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x02, 0x63, 0x6e, 0x42, 0x0d, 0x5a, 0x0b, 0x67, 0x61, 0x6d, 0x65, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var (
M pkg/gamemanager/statetoken.proto => pkg/gamemanager/statetoken.proto +4 -3
@@ 1,9 1,10 @@ syntax = "proto3"; package gamemanager; option go_package = "gamemanager"; message StateToken { string StoryID = 1; string CurrentRoom = 2; repeated string Conditions = 3; string StoryID = 1 [json_name="sid"]; string CurrentRoom = 2 [json_name="cr"]; repeated string Conditions = 3 [json_name="cn"]; } \ No newline at end of file
R pkg/gamemanager/config.go => pkg/gamemanager/storybook.go +15 -5
@@ 1,15 1,21 @@ package gamemanager import ( "errors" "fmt" "io/ioutil" "log" "os" "path/filepath" "gopkg.in/yaml.v2" "gopkg.in/yaml.v3" ) type Config struct { // ErrInvalidStoryDirectory occurs when the config specifies a nonexistent story directory. var ErrInvalidStoryDirectory = errors.New("story directory does not exist") // StoryBook contains information on all of the stories loaded from disk. type StoryBook struct { Stories map[string]Story `yaml:"stories" json:"stories"` } @@ 27,10 33,14 @@ type StoryMetadata struct { Author string `yaml:"author"` } func NewConfig(root string) (Config, error) { // NewStoryBook makes a new Config object and loads in all of the stories from the given path. func NewStoryBook(root string) (StoryBook, error) { var files []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if info == nil { return fmt.Errorf("%w: %s", ErrInvalidStoryDirectory, root) } if info.IsDir() { return nil @@ } 39,10 49,10 @@ func NewConfig(root string) (Config, error) { return nil }) if err != nil { panic(err) log.Fatalf("Couldn't load stories from disk: %s", err) } c := Config{ c := StoryBook{ Stories: map[string]Story{}, }
R pkg/web/banners.go => pkg/gemserver/banners.go +1 -1
R pkg/web/router.go => pkg/gemserver/router.go +3 -3
@@ 1,4 1,4 @@ package web package gemserver import ( @@ "fmt" 11,7 11,7 @@ import ( ) // StartRouter starts the gig server for handling Gemini requests. func StartRouter(gm *gamemanager.GameManager) { func StartRouter(gm *gamemanager.GameManager, gc ServerConfig) { //https://github.com/golangci/golangci-lint/issues/741 //nolint:staticcheck @@ pkger.Include("/static") 34,5 34,5 @@ func StartRouter(gm *gamemanager.GameManager) { g.Handle("/game/:statetoken", handleGame(gm, sceneRenderer)) g.Handle("/docs*", handleStatic) panic(g.Run("my.crt", "my.key")) panic(g.Run(fmt.Sprintf(":%d", gc.Port), gc.CertFile, gc.KeyFile)) }
R pkg/web/routes.go => pkg/gemserver/routes.go +11 -5
@@ 1,4 1,4 @@ package web package gemserver import ( @@ "fmt" 66,9 66,15 @@ func handleGame(gm *gamemanager.GameManager, renderer scenerenderer.Template) fu return fmt.Errorf("could not find room: %w", roomErr) } renderedRoom, renderErr := renderer.ExecuteTemplate(gameState, currentRoom.Description) if renderErr != nil { return fmt.Errorf("could not render room %w", renderErr) roomDesc := currentRoom.Description if gm.EngineConfig.RenderDescriptions { renderedRoom, renderErr := renderer.ExecuteTemplate(gameState, currentRoom.Description) if renderErr != nil { return fmt.Errorf("could not render room %w", renderErr) } roomDesc = renderedRoom } @@ speculativeStates, speculationErr := gm.ConstructSpeculativeStates(gameState, currentRoom) 78,7 84,7 @@ func handleGame(gm *gamemanager.GameManager, renderer scenerenderer.Template) fu return c.Render("gemif:/static/templates/room.gmi.tmpl", map[string]interface{}{ "RoomName": currentRoom.Name, "Description": renderedRoom, "Description": roomDesc, "Actions": speculativeStates, }) }
A pkg/gemserver/serverconfig.go => pkg/gemserver/serverconfig.go +8 -0
@@ 0,0 1,8 @@ package gemserver type ServerConfig struct { Domain string Port int64 CertFile string KeyFile string }
R pkg/web/template.go => pkg/gemserver/template.go +1 -1
A sample.config.toml => sample.config.toml +10 -0
@@ 0,0 1,10 @@ [gemserver] domain = "gemif.fedi.farm" port = 1967 cert_file = "./itsa.crt" key_file = "./anda.key" [engine] stories_dir = "./stories" render_descriptions = true statetoken_format = "proto" \ No newline at end of file
M static/docs/api.gmi => static/docs/api.gmi +1 -1
@@ 3,6 3,6 @@ So how does this work in the confines of the Gemini protocol? Making a call to `/start` creates you a new instance of the game, serializes it, and encodes it to base64. This is your `state token`. You are redirected to `/game/[state token]`. Making a call to `/start` creates you a new instance of the game, serializes via protobuf it, and encodes it to base64. This is your `state token`. You are redirected to `/game/[state token]`. `/game/[state token]` renders your current scene. A "speculative state" is calculated for each exit in the current scene and each exit link is rendered as a link to `/game/[speculative state token]`.