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"
)
// 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{

View File

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

View File

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

View File

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

View File

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

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 {
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"
"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 {

View File

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

View File

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