diff options
-rw-r--r-- | config.go | 105 | ||||
-rw-r--r-- | connect.go | 27 | ||||
-rw-r--r-- | content.go | 64 | ||||
-rw-r--r-- | doc/config.md | 14 | ||||
-rw-r--r-- | doc/media_pools.md | 53 | ||||
-rw-r--r-- | formspec.go | 2 | ||||
-rw-r--r-- | process.go | 33 | ||||
-rw-r--r-- | proxy.go | 2 | ||||
-rw-r--r-- | server_conn.go | 4 |
9 files changed, 203 insertions, 101 deletions
@@ -2,6 +2,7 @@ package proxy import ( "encoding/json" + "fmt" "log" "os" "sync" @@ -23,10 +24,12 @@ var configMu sync.RWMutex var loadConfigOnce sync.Once type Server struct { - Name string - Addr string - TexturePool string - Fallbacks []string + Name string + Addr string + MediaPool string + Fallbacks []string + + dynamic bool } // A Config contains information from the configuration file @@ -96,17 +99,17 @@ func UniquePoolServers() [][]Server { // every server needs a texturePool property for _, srv := range conf.Servers { - if len(srv.TexturePool) == 0 { - srv.TexturePool = srv.Name + if len(srv.MediaPool) == 0 { + srv.MediaPool = srv.Name } } // map all to.. map of slices for _, srv := range conf.Servers { - if srvs[srv.TexturePool] != nil { - srvs[srv.TexturePool] = append(srvs[srv.TexturePool], srv) + if srvs[srv.MediaPool] != nil { + srvs[srv.MediaPool] = append(srvs[srv.MediaPool], srv) } else { - srvs[srv.TexturePool] = []Server{srv} + srvs[srv.MediaPool] = []Server{srv} } } @@ -121,40 +124,62 @@ func UniquePoolServers() [][]Server { // AddServer dynamically configures a new Server at runtime. // Servers added in this way are ephemeral and will be lost // when the proxy shuts down. -// For the media to work you have to specify an alternative -// media source that is always available, even if the server -// is offline. -// WARNING: Reloading the configuration file deletes the -// servers added using this function. -func AddServer(server Server) bool { +// The server must be part of a media pool with at least one +// other member. At least one of the other members always +// needs to be reachable. +func AddServer(s Server) bool { configMu.Lock() defer configMu.Unlock() + s.dynamic = true + for _, srv := range config.Servers { - if srv.Name == server.Name { + if srv.Name == s.Name { return false } } - config.Servers = append(config.Servers, server) + var poolMembers bool + for _, srv := range config.Servers { + if srv.MediaPool == s.MediaPool { + poolMembers = true + } + } + + if !poolMembers { + return false + } + + config.Servers = append(config.Servers, s) return true } // RmServer deletes a Server from the Config at runtime. -// Any server can be deleted this way, not just the ones -// added using AddServer. -// WARNING: Reloading the configuration files re-adds the -// servers it contains that were deleted using this function. -func RmServer(name string) { +// Only servers added using AddServer can be deleted at runtime. +// Returns true on success or if the server doesn't exist. +func RmServer(name string) bool { configMu.Lock() defer configMu.Unlock() for i, srv := range config.Servers { if srv.Name == name { + if srv.dynamic { + return false + } + + // Can't remove server if players are connected to it + for cc := range Clts() { + if cc.ServerName() == name { + return false + } + } + config.Servers = append(config.Servers[:i], config.Servers[1+i:]...) - return + return true } } + + return true } // FallbackServers returns a slice of server names that @@ -225,6 +250,40 @@ func LoadConfig() error { return err } + // Dynamic servers shouldn't be deleted silently. +DynLoop: + for _, srv := range oldConf.Servers { + if srv.dynamic { + config.Servers = append(config.Servers, srv) + } else { + for _, s := range config.Servers { + if srv.Name == s.Name { + continue DynLoop + } + } + + for cc := range Clts() { + if cc.ServerName() == srv.Name { + config = oldConf + return fmt.Errorf("can't delete server %s with players", srv.Name) + } + } + } + } + + for i, srv := range config.Servers { + for _, s := range config.Servers { + if srv.Name == s.Name { + config = oldConf + return fmt.Errorf("duplicate server %s", s.Name) + } + } + + if srv.MediaPool == "" { + config.Servers[i].MediaPool = srv.Name + } + } + log.Print("load config") return nil } @@ -17,15 +17,10 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn { } cc.mu.RUnlock() - conf := Conf() - var prefix string - for _, srv := range conf.Servers { + var mediaPool string + for _, srv := range Conf().Servers { if srv.Name == name { - if len(srv.TexturePool) == 0 { - prefix = srv.Name - } else { - prefix = srv.TexturePool - } + mediaPool = srv.MediaPool } } @@ -36,7 +31,7 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn { initCh: make(chan struct{}), clt: cc, name: name, - prefix: prefix, + mediaPool: mediaPool, aos: make(map[mt.AOID]struct{}), particleSpawners: make(map[mt.ParticleSpawnerID]struct{}), sounds: make(map[mt.SoundID]struct{}), @@ -53,15 +48,15 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn { return sc } -func connectContent(conn net.Conn, name, userName, prefix string) (*contentConn, error) { +func connectContent(conn net.Conn, name, userName, mediaPool string) (*contentConn, error) { logPrefix := fmt.Sprintf("[content %s] ", name) cc := &contentConn{ - Peer: mt.Connect(conn), - logger: log.New(logWriter, logPrefix, log.LstdFlags|log.Lmsgprefix), - doneCh: make(chan struct{}), - name: name, - userName: userName, - prefix: prefix, + Peer: mt.Connect(conn), + logger: log.New(logWriter, logPrefix, log.LstdFlags|log.Lmsgprefix), + doneCh: make(chan struct{}), + name: name, + userName: userName, + mediaPool: mediaPool, } if err := cc.addDefaultTextures(); err != nil { @@ -35,8 +35,6 @@ type contentConn struct { logger *log.Logger - prefix string - cstate clientState cstateMu sync.RWMutex name, userName string @@ -47,6 +45,8 @@ type contentConn struct { salt, srpA, a, srpK []byte } + mediaPool string + itemDefs []mt.ItemDef aliases []struct{ Alias, Orig string } @@ -359,21 +359,21 @@ func muxItemDefs(conns []*contentConn) ([]mt.ItemDef, []struct{ Alias, Orig stri def.Name = "hand" } - prepend(cc.prefix, &def.Name) - prependTexture(cc.prefix, &def.InvImg) - prependTexture(cc.prefix, &def.WieldImg) - prepend(cc.prefix, &def.PlacePredict) - prepend(cc.prefix, &def.PlaceSnd.Name) - prepend(cc.prefix, &def.PlaceFailSnd.Name) - prependTexture(cc.prefix, &def.Palette) - prependTexture(cc.prefix, &def.InvOverlay) - prependTexture(cc.prefix, &def.WieldOverlay) + prepend(cc.mediaPool, &def.Name) + prependTexture(cc.mediaPool, &def.InvImg) + prependTexture(cc.mediaPool, &def.WieldImg) + prepend(cc.mediaPool, &def.PlacePredict) + prepend(cc.mediaPool, &def.PlaceSnd.Name) + prepend(cc.mediaPool, &def.PlaceFailSnd.Name) + prependTexture(cc.mediaPool, &def.Palette) + prependTexture(cc.mediaPool, &def.InvOverlay) + prependTexture(cc.mediaPool, &def.WieldOverlay) itemDefs = append(itemDefs, def) } for _, alias := range cc.aliases { - prepend(cc.prefix, &alias.Alias) - prepend(cc.prefix, &alias.Orig) + prepend(cc.mediaPool, &alias.Alias) + prepend(cc.mediaPool, &alias.Orig) aliases = append(aliases, struct{ Alias, Orig string }{ Alias: alias.Alias, @@ -431,25 +431,25 @@ func muxNodeDefs(conns []*contentConn) (nodeDefs []mt.NodeDef, p0Map param0Map, } def.Param0 = param0 - prepend(cc.prefix, &def.Name) - prepend(cc.prefix, &def.Mesh) + prepend(cc.mediaPool, &def.Name) + prepend(cc.mediaPool, &def.Mesh) for i := range def.Tiles { - prependTexture(cc.prefix, &def.Tiles[i].Texture) + prependTexture(cc.mediaPool, &def.Tiles[i].Texture) } for i := range def.OverlayTiles { - prependTexture(cc.prefix, &def.OverlayTiles[i].Texture) + prependTexture(cc.mediaPool, &def.OverlayTiles[i].Texture) } for i := range def.SpecialTiles { - prependTexture(cc.prefix, &def.SpecialTiles[i].Texture) + prependTexture(cc.mediaPool, &def.SpecialTiles[i].Texture) } - prependTexture(cc.prefix, &def.Palette) + prependTexture(cc.mediaPool, &def.Palette) for k, v := range def.ConnectTo { def.ConnectTo[k] = p0Map[cc.name][v] } - prepend(cc.prefix, &def.FootstepSnd.Name) - prepend(cc.prefix, &def.DiggingSnd.Name) - prepend(cc.prefix, &def.DugSnd.Name) - prepend(cc.prefix, &def.DigPredict) + prepend(cc.mediaPool, &def.FootstepSnd.Name) + prepend(cc.mediaPool, &def.DiggingSnd.Name) + prepend(cc.mediaPool, &def.DugSnd.Name) + prepend(cc.mediaPool, &def.DigPredict) nodeDefs = append(nodeDefs, def) param0++ @@ -468,7 +468,7 @@ func muxMedia(conns []*contentConn) []mediaFile { for _, cc := range conns { <-cc.done() for _, f := range cc.media { - prepend(cc.prefix, &f.name) + prepend(cc.mediaPool, &f.name) media = append(media, f) } } @@ -499,7 +499,7 @@ func muxContent(userName string) (itemDefs []mt.ItemDef, aliases []struct{ Alias for _, pools := range UniquePoolServers() { var addr *net.UDPAddr found := false - + for _, srv := range pools { addr, err = net.ResolveUDPAddr("udp", srv.Addr) if err != nil { @@ -514,10 +514,10 @@ func muxContent(userName string) (itemDefs []mt.ItemDef, aliases []struct{ Alias // get prefix of server var prefix string - if len(srv.TexturePool) == 0 { + if len(srv.MediaPool) == 0 { prefix = srv.Name } else { - prefix = srv.TexturePool + prefix = srv.MediaPool } var cc *contentConn @@ -613,7 +613,7 @@ func prependTexture(prep string, t *mt.Texture) { func (sc *ServerConn) prependInv(inv mt.Inv) { for k, l := range inv { for i := range l.Stacks { - prepend(sc.prefix, &inv[k].InvList.Stacks[i].Name) + prepend(sc.mediaPool, &inv[k].InvList.Stacks[i].Name) } } } @@ -622,28 +622,28 @@ func (sc *ServerConn) prependHUD(t mt.HUDType, cmdIface mt.ToCltCmd) { pa := func(cmd *mt.ToCltAddHUD) { switch t { case mt.StatbarHUD: - prepend(sc.prefix, &cmd.Text2) + prepend(sc.mediaPool, &cmd.Text2) fallthrough case mt.ImgHUD: fallthrough case mt.ImgWaypointHUD: fallthrough case mt.ImgWaypointHUD + 1: - prepend(sc.prefix, &cmd.Text) + prepend(sc.mediaPool, &cmd.Text) } } pc := func(cmd *mt.ToCltChangeHUD) { switch t { case mt.StatbarHUD: - prepend(sc.prefix, &cmd.Text2) + prepend(sc.mediaPool, &cmd.Text2) fallthrough case mt.ImgHUD: fallthrough case mt.ImgWaypointHUD: fallthrough case mt.ImgWaypointHUD + 1: - prepend(sc.prefix, &cmd.Text) + prepend(sc.mediaPool, &cmd.Text) } } diff --git a/doc/config.md b/doc/config.md index 76aae2b..4e90976 100644 --- a/doc/config.md +++ b/doc/config.md @@ -13,13 +13,11 @@ This is an example configuration file with two servers. Remember to install "Servers": [ { "Name": "default_server", - "Addr": "minetest.local:30000", - "TexturePool": "mt_game" + "Addr": "minetest.local:30000" }, { "Name": "some_other_server", - "Addr": "minetest.local:30001", - "TexturePool": "mt_game" + "Addr": "minetest.local:30001" } ] } @@ -119,11 +117,13 @@ Default: "" Description: The network address and port of an internal server. ``` -> `Server.TexturePool` +> `Server.MediaPool` ``` Type: string -Default: "" -Description: The texture Pool the server will be mapped to default is server name. +Default: Server.Name +Description: The media pool this server will be part of. +See [media_pools.md](https://github.com/HimbeerserverDE/mt-multiserver-proxy/blob/main/doc/media_pools.md) +for more information. ``` > `Server.Fallback` diff --git a/doc/media_pools.md b/doc/media_pools.md new file mode 100644 index 0000000..d2bf68b --- /dev/null +++ b/doc/media_pools.md @@ -0,0 +1,53 @@ +# Media Pools + +All servers must be part of a media pool. By default the name of the server +is used. + +## Background +When the proxy sends any content-related packets to the client, +it prefixes any content names such as node names or media file names +with the media pool of the current server and an underscore. +The purpose of this is to allow servers to have different media +with the same name and to avoid some other multiplexing issues. + +## When to use media pools? +In general, custom media pools are not required. +There are reasons to use them: +- reducing memory and storage usage on the client +- dynamically adding servers + +### Reducing RAM and disk usage +The client has to store all media it receives in memory and in its cache. +Minetest doesn't do this very efficiently: Identical files with different +names will not share memory, a copy will be made. Even if they did share +memory the references would still consume memory themselves but that would +probably be negligable. + +This may not look like a big issue but it is. Many machines, especially +phones, still only have 4 GB of RAM or even less. It's quite easy to +exceed this limit even with lightweight or basic subgames. This will make +devices that don't have enough memory unable to connect. The game will crash +while downloading media. + +The unnecessarily redundant caching will fill the permanent storage with +unneeded files too. This isn't as big of a problem as the cache isn't +(or at least shouldn't) be required for the engine to work. However +inexperienced players are going to wonder where their disk space is going. + +### Dynamic servers +These are a whole other mess but all you need to know is that they won't work +without media pools. The reason is that connected clients can't get the new +content without reconnecting due to engine restrictions. Media pools are +pushed to the client when it connects. This requires the first server of the +media pool to be reachable. This means you can make a dummy server for the +media and prevent players from connecting to it, or just use a hub server +as the media master. + +## How to use media pools? +Simply specify the name of the media pool you'd like the server to be part of +in the MediaPool field of the server definition. All server you do this for +will be part of the pool. Alternatively you can specify the name of another +server if that server doesn't have a custom media pool set or if it's the same +as its name. This will result in the servers being in a media pool that has +the same name as that server. You can use it to your advantage when creating +and naming dummy servers. diff --git a/formspec.go b/formspec.go index 169d2f8..3ffa7ac 100644 --- a/formspec.go +++ b/formspec.go @@ -13,7 +13,7 @@ func (sc *ServerConn) prependFormspec(fs *string) { for i, sub := range subs { if textureName.MatchString(sub) && !strings.Contains(sub, " ") { - prepend(sc.prefix, &subs[i]) + prepend(sc.mediaPool, &subs[i]) } } @@ -447,12 +447,7 @@ func (cc *ClientConn) process(pkt mt.Pkt) { go func(done chan<- struct{}) { result, isCmd := onChatMsg(cc, cmd) if !isCmd { - if srv == nil { - cc.Log("->", "no server") - return - } - - srv.Send(pkt) + forward(pkt) } else if result != "" { cc.SendChatMsg(result) } @@ -635,7 +630,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { handStack := mt.Stack{ Item: mt.Item{ - Name: sc.prefix + "_hand", + Name: sc.mediaPool + "_hand", }, Count: 1, } @@ -753,7 +748,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { break } - prepend(sc.prefix, &cmd.Filename) + prepend(sc.mediaPool, &cmd.Filename) if cmd.ShouldCache { cacheMedia(mediaFile{ name: cmd.Filename, @@ -763,17 +758,17 @@ func (sc *ServerConn) process(pkt mt.Pkt) { } case *mt.ToCltSkyParams: for i := range cmd.Textures { - prependTexture(sc.prefix, &cmd.Textures[i]) + prependTexture(sc.mediaPool, &cmd.Textures[i]) } case *mt.ToCltSunParams: - prependTexture(sc.prefix, &cmd.Texture) - prependTexture(sc.prefix, &cmd.ToneMap) - prependTexture(sc.prefix, &cmd.Rise) + prependTexture(sc.mediaPool, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.ToneMap) + prependTexture(sc.mediaPool, &cmd.Rise) case *mt.ToCltMoonParams: - prependTexture(sc.prefix, &cmd.Texture) - prependTexture(sc.prefix, &cmd.ToneMap) + prependTexture(sc.mediaPool, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.ToneMap) case *mt.ToCltSetHotbarParam: - prependTexture(sc.prefix, &cmd.Img) + prependTexture(sc.mediaPool, &cmd.Img) case *mt.ToCltUpdatePlayerList: if !clt.playerListInit { clt.playerListInit = true @@ -791,7 +786,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { } } case *mt.ToCltSpawnParticle: - prependTexture(sc.prefix, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.Texture) sc.globalParam0(&cmd.NodeParam0) case *mt.ToCltBlkData: for i := range cmd.Blk.Param0 { @@ -810,14 +805,14 @@ func (sc *ServerConn) process(pkt mt.Pkt) { case *mt.ToCltAddNode: sc.globalParam0(&cmd.Node.Param0) case *mt.ToCltAddParticleSpawner: - prependTexture(sc.prefix, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.Texture) sc.swapAOID(&cmd.AttachedAOID) sc.globalParam0(&cmd.NodeParam0) sc.particleSpawners[cmd.ID] = struct{}{} case *mt.ToCltDelParticleSpawner: delete(sc.particleSpawners, cmd.ID) case *mt.ToCltPlaySound: - prepend(sc.prefix, &cmd.Name) + prepend(sc.mediaPool, &cmd.Name) sc.swapAOID(&cmd.SrcAOID) if cmd.Loop { sc.sounds[cmd.ID] = struct{}{} @@ -842,7 +837,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { sc.prependFormspec(&cmd.Formspec) case *mt.ToCltMinimapModes: for i := range cmd.Modes { - prependTexture(sc.prefix, &cmd.Modes[i].Texture) + prependTexture(sc.mediaPool, &cmd.Modes[i].Texture) } case *mt.ToCltNodeMetasChanged: for k := range cmd.Changed { @@ -16,7 +16,7 @@ import ( const ( latestSerializeVer = 28 latestProtoVer = 39 - versionString = "5.5.0-dev-83a7b48bb" + versionString = "5.4.1" maxPlayerNameLen = 20 bytesPerMediaBunch = 5000 ) diff --git a/server_conn.go b/server_conn.go index 10ca868..ba229d1 100644 --- a/server_conn.go +++ b/server_conn.go @@ -17,8 +17,6 @@ type ServerConn struct { clt *ClientConn mu sync.RWMutex - prefix string - logger *log.Logger cstate clientState @@ -31,6 +29,8 @@ type ServerConn struct { salt, srpA, a, srpK []byte } + mediaPool string + inv mt.Inv detachedInvs []string |