diff --git a/chat.go b/chat.go index 61e8ca8..6257c8b 100644 --- a/chat.go +++ b/chat.go @@ -9,6 +9,10 @@ import ( "github.com/anon55555/mt" ) +// ChatCmdTimeout is the time needed until a user is warned +// about a chat command that's taking long to execute. +var ChatCmdTimeout = 10 * time.Second + // SendChatMsg sends a chat message to the ClientConn. func (cc *ClientConn) SendChatMsg(msg ...string) { cc.SendCmd(&mt.ToCltChatMsg{ diff --git a/config.go b/config.go index 3b4a6ba..c3f8dfd 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package proxy import ( "encoding/json" + "fmt" "log" "os" "sync" @@ -25,7 +26,10 @@ var loadConfigOnce sync.Once type Server struct { Name string Addr string + MediaPool string Fallbacks []string + + dynamic bool } // A Config contains information from the configuration file @@ -87,34 +91,78 @@ func Conf() Config { return config } -// AddServer appends a server to the list of configured servers. -func AddServer(server Server) bool { +// PoolServers returns all media pools and their member servers. +func PoolServers() map[string][]Server { + var srvs = make(map[string][]Server) + conf := Conf() + + // map all to.. map of slices + for _, srv := range conf.Servers { + srvs[srv.MediaPool] = append(srvs[srv.MediaPool], srv) + } + + return srvs +} + +// AddServer dynamically configures a new Server at runtime. +// Servers added in this way are ephemeral and will be lost +// when the proxy shuts down. +// 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 } -// DelServer removes a server based on name. -func DelServer(name string) bool { +// RmServer deletes a Server from the Config at runtime. +// 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 true } } - return false + return true } // FallbackServers returns a slice of server names that @@ -185,6 +233,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 } diff --git a/connect.go b/connect.go index 78f6184..746caa7 100644 --- a/connect.go +++ b/connect.go @@ -17,13 +17,21 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn { } cc.mu.RUnlock() - prefix := fmt.Sprintf("[server %s] ", name) + var mediaPool string + for _, srv := range Conf().Servers { + if srv.Name == name { + mediaPool = srv.MediaPool + } + } + + logPrefix := fmt.Sprintf("[server %s] ", name) sc := &ServerConn{ Peer: mt.Connect(conn), - logger: log.New(logWriter, prefix, log.LstdFlags|log.Lmsgprefix), + logger: log.New(logWriter, logPrefix, log.LstdFlags|log.Lmsgprefix), initCh: make(chan struct{}), clt: cc, name: name, + mediaPool: mediaPool, aos: make(map[mt.AOID]struct{}), particleSpawners: make(map[mt.ParticleSpawnerID]struct{}), sounds: make(map[mt.SoundID]struct{}), @@ -40,14 +48,15 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn { return sc } -func connectContent(conn net.Conn, name, userName string) (*contentConn, error) { - prefix := fmt.Sprintf("[content %s] ", name) +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, prefix, log.LstdFlags|log.Lmsgprefix), - doneCh: make(chan struct{}), - name: name, - userName: userName, + 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 { diff --git a/content.go b/content.go index ed7a6ce..9c1c146 100644 --- a/content.go +++ b/content.go @@ -45,6 +45,8 @@ type contentConn struct { salt, srpA, a, srpK []byte } + mediaPool string + itemDefs []mt.ItemDef aliases []struct{ Alias, Orig string } @@ -357,21 +359,21 @@ func muxItemDefs(conns []*contentConn) ([]mt.ItemDef, []struct{ Alias, Orig stri def.Name = "hand" } - prepend(cc.name, &def.Name) - prependTexture(cc.name, &def.InvImg) - prependTexture(cc.name, &def.WieldImg) - prepend(cc.name, &def.PlacePredict) - prepend(cc.name, &def.PlaceSnd.Name) - prepend(cc.name, &def.PlaceFailSnd.Name) - prependTexture(cc.name, &def.Palette) - prependTexture(cc.name, &def.InvOverlay) - prependTexture(cc.name, &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.name, &alias.Alias) - prepend(cc.name, &alias.Orig) + prepend(cc.mediaPool, &alias.Alias) + prepend(cc.mediaPool, &alias.Orig) aliases = append(aliases, struct{ Alias, Orig string }{ Alias: alias.Alias, @@ -429,25 +431,25 @@ func muxNodeDefs(conns []*contentConn) (nodeDefs []mt.NodeDef, p0Map param0Map, } def.Param0 = param0 - prepend(cc.name, &def.Name) - prepend(cc.name, &def.Mesh) + prepend(cc.mediaPool, &def.Name) + prepend(cc.mediaPool, &def.Mesh) for i := range def.Tiles { - prependTexture(cc.name, &def.Tiles[i].Texture) + prependTexture(cc.mediaPool, &def.Tiles[i].Texture) } for i := range def.OverlayTiles { - prependTexture(cc.name, &def.OverlayTiles[i].Texture) + prependTexture(cc.mediaPool, &def.OverlayTiles[i].Texture) } for i := range def.SpecialTiles { - prependTexture(cc.name, &def.SpecialTiles[i].Texture) + prependTexture(cc.mediaPool, &def.SpecialTiles[i].Texture) } - prependTexture(cc.name, &def.Palette) + prependTexture(cc.mediaPool, &def.Palette) for k, v := range def.ConnectTo { def.ConnectTo[k] = p0Map[cc.name][v] } - prepend(cc.name, &def.FootstepSnd.Name) - prepend(cc.name, &def.DiggingSnd.Name) - prepend(cc.name, &def.DugSnd.Name) - prepend(cc.name, &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++ @@ -466,7 +468,7 @@ func muxMedia(conns []*contentConn) []mediaFile { for _, cc := range conns { <-cc.done() for _, f := range cc.media { - prepend(cc.name, &f.name) + prepend(cc.mediaPool, &f.name) media = append(media, f) } } @@ -494,27 +496,37 @@ func muxRemotes(conns []*contentConn) []string { func muxContent(userName string) (itemDefs []mt.ItemDef, aliases []struct{ Alias, Orig string }, nodeDefs []mt.NodeDef, p0Map param0Map, p0SrvMap param0SrvMap, media []mediaFile, remotes []string, err error) { var conns []*contentConn - for _, srv := range Conf().Servers { + +PoolLoop: + for _, pool := range PoolServers() { var addr *net.UDPAddr - addr, err = net.ResolveUDPAddr("udp", srv.Addr) - if err != nil { - return + + for _, srv := range pool { + addr, err = net.ResolveUDPAddr("udp", srv.Addr) + if err != nil { + continue + } + + var conn *net.UDPConn + conn, err = net.DialUDP("udp", nil, addr) + if err != nil { + continue + } + + var cc *contentConn + cc, err = connectContent(conn, srv.Name, userName, srv.MediaPool) + if err != nil { + continue + } + defer cc.Close() + + conns = append(conns, cc) + continue PoolLoop } - var conn *net.UDPConn - conn, err = net.DialUDP("udp", nil, addr) - if err != nil { - return - } - - var cc *contentConn - cc, err = connectContent(conn, srv.Name, userName) - if err != nil { - return - } - defer cc.Close() - - conns = append(conns, cc) + // There's a pool with no reachable servers. + // We can't safely let clients join. + return } itemDefs, aliases = muxItemDefs(conns) @@ -594,7 +606,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.name, &inv[k].InvList.Stacks[i].Name) + prepend(sc.mediaPool, &inv[k].InvList.Stacks[i].Name) } } } @@ -603,28 +615,28 @@ func (sc *ServerConn) prependHUD(t mt.HUDType, cmdIface mt.ToCltCmd) { pa := func(cmd *mt.ToCltAddHUD) { switch t { case mt.StatbarHUD: - prepend(sc.name, &cmd.Text2) + prepend(sc.mediaPool, &cmd.Text2) fallthrough case mt.ImgHUD: fallthrough case mt.ImgWaypointHUD: fallthrough case mt.ImgWaypointHUD + 1: - prepend(sc.name, &cmd.Text) + prepend(sc.mediaPool, &cmd.Text) } } pc := func(cmd *mt.ToCltChangeHUD) { switch t { case mt.StatbarHUD: - prepend(sc.name, &cmd.Text2) + prepend(sc.mediaPool, &cmd.Text2) fallthrough case mt.ImgHUD: fallthrough case mt.ImgWaypointHUD: fallthrough case mt.ImgWaypointHUD + 1: - prepend(sc.name, &cmd.Text) + prepend(sc.mediaPool, &cmd.Text) } } diff --git a/doc/config.md b/doc/config.md index a7d9ba3..4e90976 100644 --- a/doc/config.md +++ b/doc/config.md @@ -117,6 +117,15 @@ Default: "" Description: The network address and port of an internal server. ``` +> `Server.MediaPool` +``` +Type: string +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` ``` Type: []string 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 a8a1ea2..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.name, &subs[i]) + prepend(sc.mediaPool, &subs[i]) } } diff --git a/process.go b/process.go index 401d3cb..76e175d 100644 --- a/process.go +++ b/process.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "strings" + "time" "github.com/HimbeerserverDE/srp" "github.com/anon55555/mt" @@ -441,14 +442,28 @@ func (cc *ClientConn) process(pkt mt.Pkt) { srv.swapAOID(&cmd.Pointed.(*mt.PointedAO).ID) } case *mt.ToSrvChatMsg: - go func() { + done := make(chan struct{}) + + go func(done chan<- struct{}) { + result, isCmd := onChatMsg(cc, cmd) if !isCmd { forward(pkt) } else if result != "" { cc.SendChatMsg(result) } - }() + + close(done) + }(done) + + go func(done <-chan struct{}) { + select { + case <-done: + case <-time.After(ChatCmdTimeout): + cmdName := strings.Split(cmd.Msg, " ")[0] + cc.SendChatMsg("Command", cmdName, "is taking suspiciously long to execute.") + } + }(done) return } @@ -616,7 +631,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { handStack := mt.Stack{ Item: mt.Item{ - Name: sc.name + "_hand", + Name: sc.mediaPool + "_hand", }, Count: 1, } @@ -734,7 +749,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { break } - prepend(sc.name, &cmd.Filename) + prepend(sc.mediaPool, &cmd.Filename) if cmd.ShouldCache { cacheMedia(mediaFile{ name: cmd.Filename, @@ -744,17 +759,17 @@ func (sc *ServerConn) process(pkt mt.Pkt) { } case *mt.ToCltSkyParams: for i := range cmd.Textures { - prependTexture(sc.name, &cmd.Textures[i]) + prependTexture(sc.mediaPool, &cmd.Textures[i]) } case *mt.ToCltSunParams: - prependTexture(sc.name, &cmd.Texture) - prependTexture(sc.name, &cmd.ToneMap) - prependTexture(sc.name, &cmd.Rise) + prependTexture(sc.mediaPool, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.ToneMap) + prependTexture(sc.mediaPool, &cmd.Rise) case *mt.ToCltMoonParams: - prependTexture(sc.name, &cmd.Texture) - prependTexture(sc.name, &cmd.ToneMap) + prependTexture(sc.mediaPool, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.ToneMap) case *mt.ToCltSetHotbarParam: - prependTexture(sc.name, &cmd.Img) + prependTexture(sc.mediaPool, &cmd.Img) case *mt.ToCltUpdatePlayerList: if !clt.playerListInit { clt.playerListInit = true @@ -772,7 +787,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { } } case *mt.ToCltSpawnParticle: - prependTexture(sc.name, &cmd.Texture) + prependTexture(sc.mediaPool, &cmd.Texture) sc.globalParam0(&cmd.NodeParam0) case *mt.ToCltBlkData: for i := range cmd.Blk.Param0 { @@ -791,14 +806,14 @@ func (sc *ServerConn) process(pkt mt.Pkt) { case *mt.ToCltAddNode: sc.globalParam0(&cmd.Node.Param0) case *mt.ToCltAddParticleSpawner: - prependTexture(sc.name, &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.name, &cmd.Name) + prepend(sc.mediaPool, &cmd.Name) sc.swapAOID(&cmd.SrcAOID) if cmd.Loop { sc.sounds[cmd.ID] = struct{}{} @@ -823,7 +838,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) { sc.prependFormspec(&cmd.Formspec) case *mt.ToCltMinimapModes: for i := range cmd.Modes { - prependTexture(sc.name, &cmd.Modes[i].Texture) + prependTexture(sc.mediaPool, &cmd.Modes[i].Texture) } case *mt.ToCltNodeMetasChanged: for k := range cmd.Changed { diff --git a/proxy.go b/proxy.go index e9ad2c4..3d2f69d 100644 --- a/proxy.go +++ b/proxy.go @@ -16,7 +16,7 @@ import ( const ( latestSerializeVer = 28 latestProtoVer = 39 - versionString = "5.4.1-dev-b2596eda3" + versionString = "5.4.1" maxPlayerNameLen = 20 bytesPerMediaBunch = 5000 ) diff --git a/server_conn.go b/server_conn.go index 53c7f1a..ba229d1 100644 --- a/server_conn.go +++ b/server_conn.go @@ -29,6 +29,8 @@ type ServerConn struct { salt, srpA, a, srpK []byte } + mediaPool string + inv mt.Inv detachedInvs []string