Merge pull request #5 from HimbeerserverDE/main

fetch upstream
master
Riley 2022-05-02 19:08:06 +02:00 committed by GitHub
commit ee77906613
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 264 additions and 78 deletions

View File

@ -9,6 +9,10 @@ import (
"github.com/anon55555/mt" "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. // SendChatMsg sends a chat message to the ClientConn.
func (cc *ClientConn) SendChatMsg(msg ...string) { func (cc *ClientConn) SendChatMsg(msg ...string) {
cc.SendCmd(&mt.ToCltChatMsg{ cc.SendCmd(&mt.ToCltChatMsg{

View File

@ -2,6 +2,7 @@ package proxy
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"os" "os"
"sync" "sync"
@ -25,7 +26,10 @@ var loadConfigOnce sync.Once
type Server struct { type Server struct {
Name string Name string
Addr string Addr string
MediaPool string
Fallbacks []string Fallbacks []string
dynamic bool
} }
// A Config contains information from the configuration file // A Config contains information from the configuration file
@ -87,34 +91,78 @@ func Conf() Config {
return config return config
} }
// AddServer appends a server to the list of configured servers. // PoolServers returns all media pools and their member servers.
func AddServer(server Server) bool { 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() configMu.Lock()
defer configMu.Unlock() defer configMu.Unlock()
s.dynamic = true
for _, srv := range config.Servers { for _, srv := range config.Servers {
if srv.Name == server.Name { if srv.Name == s.Name {
return false 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 return true
} }
// DelServer removes a server based on name. // RmServer deletes a Server from the Config at runtime.
func DelServer(name string) bool { // 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() configMu.Lock()
defer configMu.Unlock() defer configMu.Unlock()
for i, srv := range config.Servers { for i, srv := range config.Servers {
if srv.Name == name { 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:]...) config.Servers = append(config.Servers[:i], config.Servers[1+i:]...)
return true return true
} }
} }
return false return true
} }
// FallbackServers returns a slice of server names that // FallbackServers returns a slice of server names that
@ -185,6 +233,40 @@ func LoadConfig() error {
return err 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") log.Print("load config")
return nil return nil
} }

View File

@ -17,13 +17,21 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn {
} }
cc.mu.RUnlock() 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{ sc := &ServerConn{
Peer: mt.Connect(conn), 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{}), initCh: make(chan struct{}),
clt: cc, clt: cc,
name: name, name: name,
mediaPool: mediaPool,
aos: make(map[mt.AOID]struct{}), aos: make(map[mt.AOID]struct{}),
particleSpawners: make(map[mt.ParticleSpawnerID]struct{}), particleSpawners: make(map[mt.ParticleSpawnerID]struct{}),
sounds: make(map[mt.SoundID]struct{}), sounds: make(map[mt.SoundID]struct{}),
@ -40,14 +48,15 @@ func connect(conn net.Conn, name string, cc *ClientConn) *ServerConn {
return sc return sc
} }
func connectContent(conn net.Conn, name, userName string) (*contentConn, error) { func connectContent(conn net.Conn, name, userName, mediaPool string) (*contentConn, error) {
prefix := fmt.Sprintf("[content %s] ", name) logPrefix := fmt.Sprintf("[content %s] ", name)
cc := &contentConn{ cc := &contentConn{
Peer: mt.Connect(conn), Peer: mt.Connect(conn),
logger: log.New(logWriter, prefix, log.LstdFlags|log.Lmsgprefix), logger: log.New(logWriter, logPrefix, log.LstdFlags|log.Lmsgprefix),
doneCh: make(chan struct{}), doneCh: make(chan struct{}),
name: name, name: name,
userName: userName, userName: userName,
mediaPool: mediaPool,
} }
if err := cc.addDefaultTextures(); err != nil { if err := cc.addDefaultTextures(); err != nil {

View File

@ -45,6 +45,8 @@ type contentConn struct {
salt, srpA, a, srpK []byte salt, srpA, a, srpK []byte
} }
mediaPool string
itemDefs []mt.ItemDef itemDefs []mt.ItemDef
aliases []struct{ Alias, Orig string } aliases []struct{ Alias, Orig string }
@ -357,21 +359,21 @@ func muxItemDefs(conns []*contentConn) ([]mt.ItemDef, []struct{ Alias, Orig stri
def.Name = "hand" def.Name = "hand"
} }
prepend(cc.name, &def.Name) prepend(cc.mediaPool, &def.Name)
prependTexture(cc.name, &def.InvImg) prependTexture(cc.mediaPool, &def.InvImg)
prependTexture(cc.name, &def.WieldImg) prependTexture(cc.mediaPool, &def.WieldImg)
prepend(cc.name, &def.PlacePredict) prepend(cc.mediaPool, &def.PlacePredict)
prepend(cc.name, &def.PlaceSnd.Name) prepend(cc.mediaPool, &def.PlaceSnd.Name)
prepend(cc.name, &def.PlaceFailSnd.Name) prepend(cc.mediaPool, &def.PlaceFailSnd.Name)
prependTexture(cc.name, &def.Palette) prependTexture(cc.mediaPool, &def.Palette)
prependTexture(cc.name, &def.InvOverlay) prependTexture(cc.mediaPool, &def.InvOverlay)
prependTexture(cc.name, &def.WieldOverlay) prependTexture(cc.mediaPool, &def.WieldOverlay)
itemDefs = append(itemDefs, def) itemDefs = append(itemDefs, def)
} }
for _, alias := range cc.aliases { for _, alias := range cc.aliases {
prepend(cc.name, &alias.Alias) prepend(cc.mediaPool, &alias.Alias)
prepend(cc.name, &alias.Orig) prepend(cc.mediaPool, &alias.Orig)
aliases = append(aliases, struct{ Alias, Orig string }{ aliases = append(aliases, struct{ Alias, Orig string }{
Alias: alias.Alias, Alias: alias.Alias,
@ -429,25 +431,25 @@ func muxNodeDefs(conns []*contentConn) (nodeDefs []mt.NodeDef, p0Map param0Map,
} }
def.Param0 = param0 def.Param0 = param0
prepend(cc.name, &def.Name) prepend(cc.mediaPool, &def.Name)
prepend(cc.name, &def.Mesh) prepend(cc.mediaPool, &def.Mesh)
for i := range def.Tiles { for i := range def.Tiles {
prependTexture(cc.name, &def.Tiles[i].Texture) prependTexture(cc.mediaPool, &def.Tiles[i].Texture)
} }
for i := range def.OverlayTiles { for i := range def.OverlayTiles {
prependTexture(cc.name, &def.OverlayTiles[i].Texture) prependTexture(cc.mediaPool, &def.OverlayTiles[i].Texture)
} }
for i := range def.SpecialTiles { 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 { for k, v := range def.ConnectTo {
def.ConnectTo[k] = p0Map[cc.name][v] def.ConnectTo[k] = p0Map[cc.name][v]
} }
prepend(cc.name, &def.FootstepSnd.Name) prepend(cc.mediaPool, &def.FootstepSnd.Name)
prepend(cc.name, &def.DiggingSnd.Name) prepend(cc.mediaPool, &def.DiggingSnd.Name)
prepend(cc.name, &def.DugSnd.Name) prepend(cc.mediaPool, &def.DugSnd.Name)
prepend(cc.name, &def.DigPredict) prepend(cc.mediaPool, &def.DigPredict)
nodeDefs = append(nodeDefs, def) nodeDefs = append(nodeDefs, def)
param0++ param0++
@ -466,7 +468,7 @@ func muxMedia(conns []*contentConn) []mediaFile {
for _, cc := range conns { for _, cc := range conns {
<-cc.done() <-cc.done()
for _, f := range cc.media { for _, f := range cc.media {
prepend(cc.name, &f.name) prepend(cc.mediaPool, &f.name)
media = append(media, f) 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) { 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 var conns []*contentConn
for _, srv := range Conf().Servers {
PoolLoop:
for _, pool := range PoolServers() {
var addr *net.UDPAddr var addr *net.UDPAddr
addr, err = net.ResolveUDPAddr("udp", srv.Addr)
if err != nil { for _, srv := range pool {
return 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 // There's a pool with no reachable servers.
conn, err = net.DialUDP("udp", nil, addr) // We can't safely let clients join.
if err != nil { return
return
}
var cc *contentConn
cc, err = connectContent(conn, srv.Name, userName)
if err != nil {
return
}
defer cc.Close()
conns = append(conns, cc)
} }
itemDefs, aliases = muxItemDefs(conns) itemDefs, aliases = muxItemDefs(conns)
@ -594,7 +606,7 @@ func prependTexture(prep string, t *mt.Texture) {
func (sc *ServerConn) prependInv(inv mt.Inv) { func (sc *ServerConn) prependInv(inv mt.Inv) {
for k, l := range inv { for k, l := range inv {
for i := range l.Stacks { 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) { pa := func(cmd *mt.ToCltAddHUD) {
switch t { switch t {
case mt.StatbarHUD: case mt.StatbarHUD:
prepend(sc.name, &cmd.Text2) prepend(sc.mediaPool, &cmd.Text2)
fallthrough fallthrough
case mt.ImgHUD: case mt.ImgHUD:
fallthrough fallthrough
case mt.ImgWaypointHUD: case mt.ImgWaypointHUD:
fallthrough fallthrough
case mt.ImgWaypointHUD + 1: case mt.ImgWaypointHUD + 1:
prepend(sc.name, &cmd.Text) prepend(sc.mediaPool, &cmd.Text)
} }
} }
pc := func(cmd *mt.ToCltChangeHUD) { pc := func(cmd *mt.ToCltChangeHUD) {
switch t { switch t {
case mt.StatbarHUD: case mt.StatbarHUD:
prepend(sc.name, &cmd.Text2) prepend(sc.mediaPool, &cmd.Text2)
fallthrough fallthrough
case mt.ImgHUD: case mt.ImgHUD:
fallthrough fallthrough
case mt.ImgWaypointHUD: case mt.ImgWaypointHUD:
fallthrough fallthrough
case mt.ImgWaypointHUD + 1: case mt.ImgWaypointHUD + 1:
prepend(sc.name, &cmd.Text) prepend(sc.mediaPool, &cmd.Text)
} }
} }

View File

@ -117,6 +117,15 @@ Default: ""
Description: The network address and port of an internal server. 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` > `Server.Fallback`
``` ```
Type: []string Type: []string

53
doc/media_pools.md Normal file
View File

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

View File

@ -13,7 +13,7 @@ func (sc *ServerConn) prependFormspec(fs *string) {
for i, sub := range subs { for i, sub := range subs {
if textureName.MatchString(sub) && !strings.Contains(sub, " ") { if textureName.MatchString(sub) && !strings.Contains(sub, " ") {
prepend(sc.name, &subs[i]) prepend(sc.mediaPool, &subs[i])
} }
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
"github.com/HimbeerserverDE/srp" "github.com/HimbeerserverDE/srp"
"github.com/anon55555/mt" "github.com/anon55555/mt"
@ -441,14 +442,28 @@ func (cc *ClientConn) process(pkt mt.Pkt) {
srv.swapAOID(&cmd.Pointed.(*mt.PointedAO).ID) srv.swapAOID(&cmd.Pointed.(*mt.PointedAO).ID)
} }
case *mt.ToSrvChatMsg: case *mt.ToSrvChatMsg:
go func() { done := make(chan struct{})
go func(done chan<- struct{}) {
result, isCmd := onChatMsg(cc, cmd) result, isCmd := onChatMsg(cc, cmd)
if !isCmd { if !isCmd {
forward(pkt) forward(pkt)
} else if result != "" { } else if result != "" {
cc.SendChatMsg(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 return
} }
@ -616,7 +631,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) {
handStack := mt.Stack{ handStack := mt.Stack{
Item: mt.Item{ Item: mt.Item{
Name: sc.name + "_hand", Name: sc.mediaPool + "_hand",
}, },
Count: 1, Count: 1,
} }
@ -734,7 +749,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) {
break break
} }
prepend(sc.name, &cmd.Filename) prepend(sc.mediaPool, &cmd.Filename)
if cmd.ShouldCache { if cmd.ShouldCache {
cacheMedia(mediaFile{ cacheMedia(mediaFile{
name: cmd.Filename, name: cmd.Filename,
@ -744,17 +759,17 @@ func (sc *ServerConn) process(pkt mt.Pkt) {
} }
case *mt.ToCltSkyParams: case *mt.ToCltSkyParams:
for i := range cmd.Textures { for i := range cmd.Textures {
prependTexture(sc.name, &cmd.Textures[i]) prependTexture(sc.mediaPool, &cmd.Textures[i])
} }
case *mt.ToCltSunParams: case *mt.ToCltSunParams:
prependTexture(sc.name, &cmd.Texture) prependTexture(sc.mediaPool, &cmd.Texture)
prependTexture(sc.name, &cmd.ToneMap) prependTexture(sc.mediaPool, &cmd.ToneMap)
prependTexture(sc.name, &cmd.Rise) prependTexture(sc.mediaPool, &cmd.Rise)
case *mt.ToCltMoonParams: case *mt.ToCltMoonParams:
prependTexture(sc.name, &cmd.Texture) prependTexture(sc.mediaPool, &cmd.Texture)
prependTexture(sc.name, &cmd.ToneMap) prependTexture(sc.mediaPool, &cmd.ToneMap)
case *mt.ToCltSetHotbarParam: case *mt.ToCltSetHotbarParam:
prependTexture(sc.name, &cmd.Img) prependTexture(sc.mediaPool, &cmd.Img)
case *mt.ToCltUpdatePlayerList: case *mt.ToCltUpdatePlayerList:
if !clt.playerListInit { if !clt.playerListInit {
clt.playerListInit = true clt.playerListInit = true
@ -772,7 +787,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) {
} }
} }
case *mt.ToCltSpawnParticle: case *mt.ToCltSpawnParticle:
prependTexture(sc.name, &cmd.Texture) prependTexture(sc.mediaPool, &cmd.Texture)
sc.globalParam0(&cmd.NodeParam0) sc.globalParam0(&cmd.NodeParam0)
case *mt.ToCltBlkData: case *mt.ToCltBlkData:
for i := range cmd.Blk.Param0 { for i := range cmd.Blk.Param0 {
@ -791,14 +806,14 @@ func (sc *ServerConn) process(pkt mt.Pkt) {
case *mt.ToCltAddNode: case *mt.ToCltAddNode:
sc.globalParam0(&cmd.Node.Param0) sc.globalParam0(&cmd.Node.Param0)
case *mt.ToCltAddParticleSpawner: case *mt.ToCltAddParticleSpawner:
prependTexture(sc.name, &cmd.Texture) prependTexture(sc.mediaPool, &cmd.Texture)
sc.swapAOID(&cmd.AttachedAOID) sc.swapAOID(&cmd.AttachedAOID)
sc.globalParam0(&cmd.NodeParam0) sc.globalParam0(&cmd.NodeParam0)
sc.particleSpawners[cmd.ID] = struct{}{} sc.particleSpawners[cmd.ID] = struct{}{}
case *mt.ToCltDelParticleSpawner: case *mt.ToCltDelParticleSpawner:
delete(sc.particleSpawners, cmd.ID) delete(sc.particleSpawners, cmd.ID)
case *mt.ToCltPlaySound: case *mt.ToCltPlaySound:
prepend(sc.name, &cmd.Name) prepend(sc.mediaPool, &cmd.Name)
sc.swapAOID(&cmd.SrcAOID) sc.swapAOID(&cmd.SrcAOID)
if cmd.Loop { if cmd.Loop {
sc.sounds[cmd.ID] = struct{}{} sc.sounds[cmd.ID] = struct{}{}
@ -823,7 +838,7 @@ func (sc *ServerConn) process(pkt mt.Pkt) {
sc.prependFormspec(&cmd.Formspec) sc.prependFormspec(&cmd.Formspec)
case *mt.ToCltMinimapModes: case *mt.ToCltMinimapModes:
for i := range cmd.Modes { for i := range cmd.Modes {
prependTexture(sc.name, &cmd.Modes[i].Texture) prependTexture(sc.mediaPool, &cmd.Modes[i].Texture)
} }
case *mt.ToCltNodeMetasChanged: case *mt.ToCltNodeMetasChanged:
for k := range cmd.Changed { for k := range cmd.Changed {

View File

@ -16,7 +16,7 @@ import (
const ( const (
latestSerializeVer = 28 latestSerializeVer = 28
latestProtoVer = 39 latestProtoVer = 39
versionString = "5.4.1-dev-b2596eda3" versionString = "5.4.1"
maxPlayerNameLen = 20 maxPlayerNameLen = 20
bytesPerMediaBunch = 5000 bytesPerMediaBunch = 5000
) )

View File

@ -29,6 +29,8 @@ type ServerConn struct {
salt, srpA, a, srpK []byte salt, srpA, a, srpK []byte
} }
mediaPool string
inv mt.Inv inv mt.Inv
detachedInvs []string detachedInvs []string