all repos — felt @ 01fccb55f077384bbb47b0525bf0de41b34929ca

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

backend token logic
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmSo+pkACgkQO3+8IhRO
Y5jRZg/7BO39hr8ZBBFGt1IxBwpQVfqSVUww0wUBaj7ynQ5rh/Cr35El8o5b6waB
P0oQDiCxaqfs8mOeaD/jaqjN2WwfzB+C7T8W2rlijQhvUpmVyqyoHwnZZCPpSi+l
s9fuBaRJs6IX/9ks8fOQ2bQaIg+blXAmBsbU+nOdmTXsdWBnMOZtfdjMwWwHOavj
sUPDsRVUccCbmr59MbgGG0p0I2ztOWBiFIK/QvS2omohBs6qqmpgAxkALmcVCtNa
zjCUHvt7PALa/pTLHsuDN7nBag6uyuUzog6iB1p0bcrbyBJMIpb8dHuWwGYFXepv
bGY6P4WCxaF4VuqAracqnDCaE1OfrROyk3r6dgGh42T/YKQEdg5j2wB5hPjgLCRy
7YLz8qUb5ipWtIbNJf1hASXuuqOPUj/9UiJJALPRR34xpLnNte2dHQ7OEN2EiLsx
7/kvRhkCSwChitMSKXID6p8qn6DhjTAFCmwunozBWguWHjHelPqddbbHZB+J7LRy
9m6EWfItDxtV7B4mC9rd4n1lYLZPaYaSik+M6Is/NQQmjFS1JGAbEbd+HxP1uxeF
uUq44eIfUE6nZRfJXiQJXbV31qhkZSMLJC5wocUQlbntQFBeSRCLgSEBiB7efJZW
iKGDLUeG2bonUPbtYtjsSBg12ZxHOnags0f3xfujcfzu5fjmkuU=
=oha/
-----END PGP SIGNATURE-----
commit

01fccb55f077384bbb47b0525bf0de41b34929ca

parent

f4513a28f7de843bcef4def5f98ce85545148a49

M admin/admin.goadmin/admin.go

@@ -48,18 +48,16 @@

if dbAdapter.CheckTable(tableKey) { mapUrl, _ := dbAdapter.GetMapImageUrl(tableKey) auxMessage, _ := dbAdapter.GetAuxMessage(tableKey) - availableTokens, _ := dbAdapter.GetTokens(tableKey, true) - activeTokens, _ := dbAdapter.GetTokens(tableKey, false) + tokens, _ := dbAdapter.GetTokens(tableKey, true) diceRolls, _ := dbAdapter.GetDiceRolls(tableKey) AddContextValue(req, "tableData", models.Table{ - Name: tableKey.Name, - Passcode: tableKey.Passcode, - DiceRolls: diceRolls, - MapImageUrl: mapUrl, - Tokens: activeTokens, - AvailableTokens: availableTokens, - AuxMessage: auxMessage, + Name: tableKey.Name, + Passcode: tableKey.Passcode, + DiceRolls: diceRolls, + MapImageUrl: mapUrl, + Tokens: tokens, + AuxMessage: auxMessage, }) } else { w.WriteHeader(404)
M gametable/server.gogametable/server.go

@@ -192,18 +192,16 @@ // get diceroll log, map, and token state

if self.dbAdapter.CheckTable(tableKey) { mapUrl, _ := self.dbAdapter.GetMapImageUrl(tableKey) auxMessage, _ := self.dbAdapter.GetAuxMessage(tableKey) - availableTokens, _ := self.dbAdapter.GetTokens(tableKey, true) - activeTokens, _ := self.dbAdapter.GetTokens(tableKey, false) + tokens, _ := self.dbAdapter.GetTokens(tableKey, false) diceRolls, _ := self.dbAdapter.GetDiceRolls(tableKey) table := models.Table{ - Name: tableKey.Name, - Passcode: tableKey.Passcode, - DiceRolls: diceRolls, - MapImageUrl: mapUrl, - Tokens: activeTokens, - AvailableTokens: availableTokens, - AuxMessage: auxMessage, + Name: tableKey.Name, + Passcode: tableKey.Passcode, + DiceRolls: diceRolls, + MapImageUrl: mapUrl, + Tokens: tokens, + AuxMessage: auxMessage, } data, err := json.Marshal(table) if err != nil {

@@ -223,8 +221,41 @@ if err != nil {

return err } } + if tableMsg.Token != nil && tableMsg.Token.Id != nil { + t := *tableMsg.Token + exists, active := self.dbAdapter.CheckToken(key, *t.Id) + if exists { + if active { + if !t.Active { + err := self.dbAdapter.ActivateToken(key, *t.Id, false) + if err != nil { + return err + } + tableMsg.Token.X = nil + tableMsg.Token.Y = nil + } else if t.X != nil && t.Y != nil { + err := self.dbAdapter.MoveToken(key, t) + if err != nil { + return err + } + } + } else { + if t.Active { + err := self.dbAdapter.ActivateToken(key, *t.Id, true) + if err != nil { + return err + } + } + } + } else { + // respond to nonextant IDs as if they were destroyed + tableMsg.Token.X = nil + tableMsg.Token.Y = nil + tableMsg.Token.Active = false + } + } - // map image change, aux message, and token addition/removal require admin authorization + // map image change, aux message, and token creation/deletion require admin authorization if tableMsg.Auth != nil { authorized, _ := self.udb.ValidateToken(*tableMsg.Auth) if authorized {

@@ -238,6 +269,22 @@ if tableMsg.AuxMsg != nil {

err := self.dbAdapter.SetAuxMessage(key, *tableMsg.AuxMsg) if err != nil { return err + } + } + if tableMsg.Token != nil { + t := *tableMsg.Token + if t.Id == nil { + id, err := self.dbAdapter.CreateToken(key, t) + if err == nil { + *tableMsg.Token.Id = id + } else { + return err + } + } else { + if t.X == nil && t.Y == nil && !t.Active { + err := self.dbAdapter.DestroyToken(key, *t.Id) + return err + } } } }
M go.modgo.mod

@@ -3,7 +3,7 @@

go 1.19 require ( - go.mongodb.org/mongo-driver v1.11.0 + go.mongodb.org/mongo-driver v1.12.0 golang.org/x/time v0.1.0 hacklab.nilfm.cc/quartzgun v0.3.0 nhooyr.io/websocket v1.8.7

@@ -15,10 +15,10 @@ github.com/klauspost/compress v1.13.6 // indirect

github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pkg/errors v0.9.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.1 // indirect - github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/text v0.7.0 // indirect )
M go.sumgo.sum

@@ -55,6 +55,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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=

@@ -68,31 +69,60 @@ github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=

github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE= go.mongodb.org/mongo-driver v1.11.0/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= +go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= +go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
M models/models.gomodels/models.go

@@ -18,21 +18,25 @@ Timestamp time.Time `json:"timestamp"`

} type Token struct { - Id string `json:"id"` - Name string `json:"name"` - SpriteUri string `json:"spriteUrl"` - X *int `json:"x"` - Y *int `json:"y"` + Id *string `json:"id" bson:"_id"` + Name string `json:"name"` + Sprite string `json:"sprite"` + W int `json:"w"` + H int `json:"h"` + OX int `json:"oX"` + OY int `json:"oY"` + X *int `json:"x"` + Y *int `json:"y"` + Active bool `json:"active"` } type Table struct { - Name string `json:"name"` - Passcode string `json:"passcode"` - MapImageUrl string `json:"mapImg"` - DiceRolls []DiceRoll `json:"diceRolls"` - Tokens []Token `json:"tokens"` - AvailableTokens []Token `json:"availableTokens"` - AuxMessage string `json:"auxMsg"` + Name string `json:"name"` + Passcode string `json:"passcode"` + MapImageUrl string `json:"mapImg"` + DiceRolls []DiceRoll `json:"diceRolls"` + Tokens []Token `json:"tokens"` + AuxMessage string `json:"auxMsg"` } type TableMessage struct {
M mongodb/adapter.gomongodb/adapter.go

@@ -5,6 +5,7 @@ "context"

"errors" "fmt" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "hacklab.nilfm.cc/felt/models"

@@ -14,7 +15,9 @@

const errNoCollection string = "collection not found: felt.%s" const errNoDocument string = "document with name/id '%s' doesn't exist in collection: %s" const errNotAString string = "document property is not a string: %s<key=%s>.%s" -const errNotAnArray string = "doccument property is not an array: %s<key=%s>.%s" +const errNotAnArray string = "document property is not an array: %s<key=%s>.%s" + +const ErrNotFound string = "this token doesn't exist at this table; forget about it" type DbAdapter interface { Init(mongoUri string) error

@@ -34,10 +37,12 @@

SetAuxMessage(table models.TableKey, message string) error GetAuxMessage(table models.TableKey) (string, error) - AddToken(table models.TableKey, token models.Token, active bool) error - RemoveToken(table models.TableKey, tokenId string, active bool) error - ModifyToken(table models.TableKey, token models.Token, active bool) error - GetTokens(table models.TableKey, active bool) ([]models.Token, error) + CheckToken(table models.TableKey, tokenId string) (bool, bool) + CreateToken(table models.TableKey, token models.Token) (string, error) + ActivateToken(table models.TableKey, tokenId string, active bool) error + MoveToken(table models.TableKey, token models.Token) error + DestroyToken(table models.TableKey, tokenId string) error + GetTokens(table models.TableKey, activeOnly bool) ([]models.Token, error) } type DbEngine struct {

@@ -284,13 +289,37 @@

return "", errors.New(fmt.Sprintf(errNoCollection, "tables")) } -func (self *DbEngine) AddToken(table models.TableKey, token models.Token, active bool) error { +func (self *DbEngine) CheckToken(table models.TableKey, tokenId string) (bool, bool) { + mongoId, err := primitive.ObjectIDFromHex(tokenId) + if err != nil { + return false, false + } tables := self.db.Collection("tables") if tables != nil { - tokenArrKey := "tokens" - if !active { - tokenArrKey = "availableTokens" + result := models.Table{} + err := tables.FindOne(self.mkCtx(10), bson.D{ + {"name", table.Name}, + {"passcode", table.Passcode}, + {"tokens", bson.E{"_id", mongoId}}, + }).Decode(&result) + if err != nil { + return false, false + } else { + active := false + for _, t := range result.Tokens { + if *t.Id == tokenId && t.Active { + active = true + } + } + return true, active } + } + return false, false +} + +func (self *DbEngine) CreateToken(table models.TableKey, token models.Token) (string, error) { + tables := self.db.Collection("tables") + if tables != nil { var result models.Table err := tables.FindOneAndUpdate( self.mkCtx(10),

@@ -299,30 +328,39 @@ {"name", table.Name},

{"passcode", table.Passcode}, }, bson.D{ - {"$push", bson.D{{tokenArrKey, token}}}, + {"$push", bson.D{{"tokens", token}}}, }, ).Decode(&result) - return err + if err == nil { + newId := result.Tokens[len(result.Tokens)-1].Id + return *newId, nil + } else { + return "", err + } } - return errors.New(fmt.Sprintf(errNoCollection, "tables")) + return "", errors.New(fmt.Sprintf(errNoCollection, "tables")) + } -func (self *DbEngine) RemoveToken(table models.TableKey, tokenId string, active bool) error { +func (self *DbEngine) ActivateToken(table models.TableKey, tokenId string, active bool) error { + mongoId, err := primitive.ObjectIDFromHex(tokenId) + if err != nil { + return err + } tables := self.db.Collection("tables") if tables != nil { - tokenArrKey := "tokens" - if !active { - tokenArrKey = "availableTokens" - } var result models.Table err := tables.FindOneAndUpdate( self.mkCtx(10), bson.D{ {"name", table.Name}, {"passcode", table.Passcode}, + {"tokens", bson.E{"_id", mongoId}}, }, bson.D{ - {"$pull", bson.D{{tokenArrKey, bson.D{{"_id", tokenId}}}}}, + {"$set", bson.D{{"tokens.$", bson.D{ + {"active", active}, + }}}}, }, ).Decode(&result) return err

@@ -330,25 +368,23 @@ }

return errors.New(fmt.Sprintf(errNoCollection, "tables")) } -func (self *DbEngine) ModifyToken(table models.TableKey, token models.Token, active bool) error { +func (self *DbEngine) MoveToken(table models.TableKey, token models.Token) error { + mongoId, err := primitive.ObjectIDFromHex(*token.Id) + if err != nil { + return err + } tables := self.db.Collection("tables") if tables != nil { - tokenArrKey := "tokens" - if !active { - tokenArrKey = "availableTokens" - } var result models.Table err := tables.FindOneAndUpdate( self.mkCtx(10), bson.D{ {"name", table.Name}, {"passcode", table.Passcode}, - {tokenArrKey, bson.E{"_id", token.Id}}, + {"tokens", bson.E{"_id", mongoId}}, }, bson.D{ - {"$set", bson.D{{tokenArrKey + ".$", bson.D{ - {"name", token.Name}, - {"spriteUri", token.SpriteUri}, + {"$set", bson.D{{"tokens.$", bson.D{ {"x", token.X}, {"y", token.Y}, }}}},

@@ -359,7 +395,30 @@ }

return errors.New(fmt.Sprintf(errNoCollection, "tables")) } -func (self *DbEngine) GetTokens(table models.TableKey, active bool) ([]models.Token, error) { +func (self *DbEngine) DestroyToken(table models.TableKey, tokenId string) error { + mongoId, err := primitive.ObjectIDFromHex(tokenId) + if err != nil { + return err + } + tables := self.db.Collection("tables") + if tables != nil { + var result models.Table + err := tables.FindOneAndUpdate( + self.mkCtx(10), + bson.D{ + {"name", table.Name}, + {"passcode", table.Passcode}, + }, + bson.D{ + {"$pull", bson.D{{"tokens", bson.D{{"_id", mongoId}}}}}, + }, + ).Decode(&result) + return err + } + return errors.New(fmt.Sprintf(errNoCollection, "tables")) +} + +func (self *DbEngine) GetTokens(table models.TableKey, activeOnly bool) ([]models.Token, error) { tables := self.db.Collection("tables") if tables != nil { var result models.Table

@@ -370,11 +429,14 @@ {"name", table.Name},

{"passcode", table.Passcode}, }).Decode(&result) if err == nil { - if active { - return result.Tokens, nil - } else { - return result.AvailableTokens, nil + tokens := []models.Token{} + for _, t := range result.Tokens { + if !activeOnly || t.Active { + tokens = append(tokens, t) + } + } + return tokens, nil } else { return nil, errors.New(fmt.Sprintf(errNoDocument, table.Name, "tables")) }
M static/admin.jsstatic/admin.js

@@ -16,6 +16,7 @@ const tokenCY = document.getElementById("token_cy");

const previewZone = document.getElementById("tokenPreview_zone"); const tokenAspect = document.getElementById("tokenKeepAspect"); const aspectLockLabel = document.getElementById("aspectLockLabel"); +const tokenZone = document.getElementById("tokenZone"); async function getTable(name, pass) { try {

@@ -41,8 +42,9 @@ if (res.ok) {

document.getElementById("input_table_name").value = name; document.getElementById("input_table_pass").value = pass; dial(); + const table = await res.json() infoHtml = "<a href='#' onclick='getTables()'>&larr; table list</a><br>"; - infoHtml += `<textarea id='auxMsgZone'>${(await res.json()).auxMsg}</textarea><br><button onclick='publishAuxMsg()'>Set Status</button>` + infoHtml += `<textarea id='auxMsgZone'>${table.auxMsg}</textarea><br><button onclick='publishAuxMsg()'>Set Status</button>` infoHtml += "<button onclick='destroyTable()'>Destroy Table</button><br/>"; infoHtml += "<input id='map_img_upload' type='file'/><button onclick='uploadMapImg()'>Upload Map</button><br/>" if (mapImgs.ok) {

@@ -70,17 +72,13 @@ tokenListHTML += `<li>${parts[parts.length - 1]} <a href="${t}" target="_blank">view</a> <button onclick="deleteImg('${t}')">Delete</button></li>\n`

} tokenListHTML += "</ul>"; fillSpriteDropdown(tokens); + redrawTokenMasterList(); } else { tokenListHTML += "<label>Sprites couldn't be retrieved</label>" } spriteZone.innerHTML = tokenListHTML; - - - tokenWrapper.style.display = "inline"; - - // also, we have to fill and toggle the tokens window } else { console.log(res.status);

@@ -90,6 +88,15 @@ setErr(`${err.name}: ${err.message}`);

} } +function redrawTokenMasterList() { + if (tokenZone) { + const headers = new Headers(); + headers.set('Authorization', 'Bearer ' + adminToken.access_token); + + const res = await fetch(`/` + } +} + function fillSpriteDropdown(tokens) { let options = "<option value=''>select</option>"; for (const t of tokens) {

@@ -127,34 +134,33 @@ }

function scaleSpritePreview(source) { if (mapImg && mapImg._image) { - console.log(mapImg); - const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; - const keepAspect = tokenAspect.checked; - const img = previewZone.children[0]; - if (img) { - if (!keepAspect || !source) { - img.width = Number(tokenWidth.value) * scaleFactor; - img.height = Number(tokenHeight.value) * scaleFactor; - } else { + const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; + const keepAspect = tokenAspect.checked; + const img = previewZone.children[0]; + if (img) { + if (!keepAspect || !source) { + img.width = Number(tokenWidth.value) * scaleFactor; + img.height = Number(tokenHeight.value) * scaleFactor; + } else { - const currentAspect = img.width/img.height; - switch (source.id) { - case "token_width": - img.width = Number(tokenWidth.value) * scaleFactor; - img.height = (img.clientWidth / img.naturalWidth) * img.naturalHeight; - tokenHeight.value = Number(tokenWidth.value)/currentAspect; - break; - case "token_height": - img.height = Number(tokenHeight.value) * scaleFactor; - img.width = (img.clientHeight / img.naturalHeight) * img.naturalWidth; - tokenWidth.value = currentAspect * Number(tokenHeight.value); - break; + const currentAspect = img.width/img.height; + switch (source.id) { + case "token_width": + img.width = Number(tokenWidth.value) * scaleFactor; + img.height = (img.clientWidth / img.naturalWidth) * img.naturalHeight; + tokenHeight.value = Number(tokenWidth.value)/currentAspect; + break; + case "token_height": + img.height = Number(tokenHeight.value) * scaleFactor; + img.width = (img.clientHeight / img.naturalHeight) * img.naturalWidth; + tokenWidth.value = currentAspect * Number(tokenHeight.value); + break; + } } + tokenCX.value = Number(tokenWidth.value)/2; + tokenCY.value = Number(tokenHeight.value)/2; + drawTokenOrigin(); } - tokenCX.value = Number(tokenWidth.value)/2; - tokenCY.value = Number(tokenHeight.value)/2; - drawTokenOrigin(); - } } }

@@ -162,19 +168,20 @@ function drawTokenOrigin() {

const img = previewZone.children[0]; const x = Number(tokenWidth.value) / Number(tokenCX.value); const y = Number(tokenHeight.value) / Number(tokenCY.value); + const origin = {x: img.width/x, y: img.height/y}; const originImg = document.createElement("img"); + originImg.src="/table/origin.png"; originImg.style.position = "absolute"; originImg.style.left = (origin.x - 2) + "px"; originImg.style.top = (origin.y - 2) + "px"; + if (previewZone.children.length > 1) { previewZone.replaceChild(originImg, previewZone.children[1]); } else { previewZone.appendChild(originImg); } - - } function reinitializeSpritePreview() {

@@ -206,24 +213,19 @@ const oY = Number(tokenCY.value);

const img = tokenSpriteDropdown[tokenSpriteDropdown.selectedIndex].value; const name = tokenName.value; - console.log("creating token"); if (!isNaN(w) && !isNaN(h) && !isNaN(oX) && !isNaN(oY) && img && name) { console.log("all green"); - const self = { - sz: [w, h], - m: L.marker(getCascadingPos(), { - icon: L.icon({ - iconUrl: img, - iconSize: [w,h], - }), - title: name, - draggable: true, - autoPan: true - }), - }; + + // create on the frontend for testing + /* + const self = NewToken(w, h, oX, oY, img, name); tokens.push(self); self.m.addTo(map); resizeMarkers(); + */ + + // really though we have to send it on the websocket and wait for it to come back + } }

@@ -236,8 +238,17 @@ }

} function sendMapImg(url) { - console.log("sending " + url); publish({mapImg: url, auth: adminToken.access_token}); +} + +function sendToken(t) { + publish({token: t, auth: adminToken.access_token}); +} + +function revokeToken(t) { + t.x = null; + t.y = null; + sendToken(t); } async function uploadMapImg() {

@@ -446,5 +457,9 @@ });

if (res.ok) { getTables(); setTableCreateFormVisible(false); + } else if (res.status === 422) { + setErr('Table name and passcode must be only alphanumeric and underscores'); + } else { + setErr('Error creating table'); } }
M static/index.htmlstatic/index.html

@@ -88,7 +88,7 @@ </details><br/>

<div id="adminWrapper_tokens"> <details id="admin_token_win" class="ui_win admin_win"><summary>tokens</summary> <button onclick="setTokenCreateFormVisible(true)">New Token</button> - <form onsubmit="return false" id="createTokenForm"> + <form onsubmit="return false" id="createTokenForm" style="display:none;"> <label>Sprite<select id="token_combobox" onchange="previewSprite(this)"></select></label><br/> <label>Name<input id="token_name"/></label><br/>

@@ -96,9 +96,9 @@ <label>Width<input type="number" id="token_width" min="1" max="9999" onchange="previewSprite(this)"/></label><label id="aspectLockLabel" for="tokenKeepAspect">&#128274;</label><input type="checkbox" checked id="tokenKeepAspect" onchange="toggleAspectLock()"/><br/>

<label>Height<input type="number" id="token_height" min="1" max="9999" onchange="previewSprite(this)"/></label><br/> <label>cX<input type="number" id="token_cx" min="0" max="9999" onchange="previewSprite(this)"/></label><br/> <label>cY<input type="number" id="token_cy" min="0" max="9999" onchange="previewSprite(this)"/></label><br/> - <div id="tokenPreview_zone"></div> <button type="submit" onclick="createToken()">Create</button> <button onclick="setTokenCreateFormVisible(false)">Cancel</button> + <div id="tokenPreview_zone"></div> </form> <div id="tokenZone"></div> </details><br/>
M static/map.jsstatic/map.js

@@ -4,7 +4,9 @@ let tokens = [];

const worldBounds = [[180, -180],[-180, 180]]; function initializeMap(mapImgUrl) { + let init = false; if (!map) { + init = true; map = L.map('map', { minZoom: 0, maxZoom: 4, crs: L.CRS.Simple }); map.on("zoomend", ()=>{resizeMarkers();scaleSpritePreview();}); }

@@ -14,7 +16,9 @@ }

mapImg = L.imageOverlay(mapImgUrl, worldBounds); mapImg.addTo(map); map.setMaxBounds(worldBounds); - map.setView([0,0], 2); + if (init) { + map.setView([0,0], 2); + } while (tokens.some(t=>t)) { tokens[0].m.removeFrom(map); tokens.shift();

@@ -37,6 +41,21 @@ const n = tokens.length;

topLeft[1] += (n+1)*5; topLeft[0] -= (n+1)*5; return topLeft; +} + +function NewToken(w, h, oX, oY, img, name, x, y) { + return { + sz: [w, h], + m: L.marker((x && y) ? [y,x] : getCascadingPos(), { + icon: L.icon({ + iconUrl: img, + iconSize: [w,h], + }), + title: name, + draggable: true, + autoPan: true + }), + }; } function addToken(token) {
M static/socket.jsstatic/socket.js

@@ -19,15 +19,17 @@ return n < 10 ? "0" + n : String(n);

} function formatDice(r) { - console.log(r); const date = new Date(r.timestamp) const p = document.createElement("p"); + const month = date.getMonth() + 1; const day = date.getDate(); const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); + p.innerHTML = `${date.getFullYear()}-${fmtLeading(month)}-${fmtLeading(day)} ${fmtLeading(hours)}:${fmtLeading(minutes)}:${fmtLeading(seconds)} ${r.player} rolled ${r.roll.length}d${r.faces} ${(r.note ? "(" + r.note + ")" : "")}<br>[${r.roll}] (total ${r.roll.reduce((a,c)=>a+c,0)})`; + return p; }
M static/style.cssstatic/style.css

@@ -147,6 +147,10 @@ .ui_win a:hover, ui_win a:active {

color: var(--fg_color); } +.ui_win ul { + max-height: 10em; +} + #admin_section { text-align: right; }