From f139b31d319d3d77d6352609018cdafb89108d94 Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Thu, 3 Sep 2026 21:13:42 -0400 Subject: [PATCH] add mod respawn --- Dockerfile | 8 +- app/config.go | 6 ++ app/types.go | 6 ++ dev/mapserver.json | 8 +- doc/config.md | 15 ++- mapobject/mcl_sign.go | 49 +++++++++ mapobject/setup.go | 131 +++++++++++++---------- public/js/map/Overlaysetup.js | 8 ++ public/js/map/overlays/RespawnOverlay.js | 119 ++++++++++++++++++++ readme.md | 3 +- respawnparser/respawnparser.go | 61 +++++++++++ web/config.go | 2 + web/respawn.go | 57 ++++++++++ web/serve.go | 4 + 14 files changed, 413 insertions(+), 64 deletions(-) create mode 100644 mapobject/mcl_sign.go create mode 100644 public/js/map/overlays/RespawnOverlay.js create mode 100644 respawnparser/respawnparser.go create mode 100644 web/respawn.go diff --git a/Dockerfile b/Dockerfile index 2833be55..328abd62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,11 @@ FROM golang:1.24.3 as go-builder COPY . /data COPY --from=bundle-builder /public/js/bundle* /data/public/js/ WORKDIR /data -RUN CGO_ENABLED=0 go build . +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 go build . FROM alpine:3.21.3 COPY --from=go-builder /data/mapserver /bin/mapserver @@ -15,4 +19,4 @@ ENV MT_CONFIG_PATH "mapserver.json" ENV MT_LOGLEVEL "INFO" ENV MT_READONLY "false" EXPOSE 8080 -ENTRYPOINT ["/bin/mapserver"] \ No newline at end of file +ENTRYPOINT ["/bin/mapserver"] diff --git a/app/config.go b/app/config.go index 036f1b89..b56bcecd 100644 --- a/app/config.go +++ b/app/config.go @@ -71,6 +71,7 @@ func ParseConfig(filename string) (*Config, error) { Digiterms: true, Digilines: true, Travelnet: true, + RespawnPlaces: true, MapserverPlayer: true, MapserverPOI: true, MapserverLabel: true, @@ -109,6 +110,10 @@ func ParseConfig(filename string) (*Config, error) { SkinsPath: "", } + respawn := RespawnConfig{ + PlacesColor: "green", + } + cfg := Config{ ConfigVersion: 1, Port: 8080, @@ -134,6 +139,7 @@ func ParseConfig(filename string) (*Config, error) { WorldPath: "./", DataPath: "./", ColorsTxtPath: "./", + Respawn: &respawn, } info, err := os.Stat(filename) diff --git a/app/types.go b/app/types.go index b4340539..7d037dfe 100644 --- a/app/types.go +++ b/app/types.go @@ -27,6 +27,7 @@ type Config struct { WorldPath string `json:"worldpath"` DataPath string `json:"datapath"` ColorsTxtPath string `json:"colorstxtpath"` + Respawn *RespawnConfig `json:"respawn"` } type MapBlockAccessorConfig struct { @@ -49,6 +50,7 @@ type MapObjectConfig struct { Digiterms bool `json:"digiterms"` Digilines bool `json:"digilines"` Travelnet bool `json:"travelnet"` + RespawnPlaces bool `json:"respawn_places"` MapserverPlayer bool `json:"mapserver_player"` MapserverPOI bool `json:"mapserver_poi"` MapserverLabel bool `json:"mapserver_label"` @@ -82,3 +84,7 @@ type SkinsConfig struct { EnableSkinsDB bool `json:"enableskinsdb"` SkinsPath string `json:"skinspath"` } + +type RespawnConfig struct { + PlacesColor string `json:"placescolor"` +} diff --git a/dev/mapserver.json b/dev/mapserver.json index d1588d90..3d87a452 100644 --- a/dev/mapserver.json +++ b/dev/mapserver.json @@ -45,6 +45,7 @@ "digiterms": true, "digilines": true, "travelnet": true, + "respawn_places": true, "mapserver_player": true, "mapserver_poi": true, "mapserver_label": true, @@ -82,5 +83,8 @@ }, "worldpath": "./", "datapath": "./", - "colorstxtpath": "./" -} \ No newline at end of file + "colorstxtpath": "./", + "respawn": { + "placescolor": "green" + } +} diff --git a/doc/config.md b/doc/config.md index 97884420..fcd28c1d 100644 --- a/doc/config.md +++ b/doc/config.md @@ -50,6 +50,7 @@ The mapserver will generate a fresh `mapserver.json` if there is none at startup "digiterms": true, "digilines": true, "travelnet": true, + "respawn_places": true, "mapserver": true, "mission": true, "jumpdrive": true, @@ -62,10 +63,13 @@ The mapserver will generate a fresh `mapserver.json` if there is none at startup "purgetime": "15s", "maxitems": 5000 }, - "skins": { - "enableskinsdb": true, - "skinspath": "/path/to/minetest/mods/skinsdb/textures" - } + "skins": { + "enableskinsdb": true, + "skinspath": "/path/to/minetest/mods/skinsdb/textures" + }, + "respawn": { + "placescolor": "green" + }, } ``` @@ -129,3 +133,6 @@ Enables support for serving/displaying custom player skins provided by the Skins The path to where SkinsDB textures are stored. This should be the SkinsDB textures directory. Example: `/path/to/minetest/mods/skinsdb/textures` + +#### respawn.placescolor +The [marker color](https://github.com/lennardv2/Leaflet.awesome-markers) of the map pin for each respawn point. diff --git a/mapobject/mcl_sign.go b/mapobject/mcl_sign.go new file mode 100644 index 00000000..7a80b23c --- /dev/null +++ b/mapobject/mcl_sign.go @@ -0,0 +1,49 @@ +package mapobject + +import ( + "regexp" + "strconv" + "strings" + "mapserver/mapobjectdb" + "mapserver/types" + + "github.com/minetest-go/mapparser" +) + +var utextNumberRE = regexp.MustCompile(`\d+`) +func decodeUText(value string) string { + // Extract all decimal numbers from: + // return {115,105,103,110,49,10,65,10,97} + matches := utextNumberRE.FindAllString(value, -1) + if len(matches) == 0 { + return "" + } + + var b strings.Builder + + for _, match := range matches { + n, err := strconv.Atoi(match) + if err != nil || n < 0 || n > 255 { + continue + } + + b.WriteByte(byte(n)) + } + + return b.String() +} + +type MclSignBlock struct { + Material string +} + +func (this *MclSignBlock) onMapObject(mbpos *types.MapBlockCoords, x, y, z int, block *mapparser.MapBlock) *mapobjectdb.MapObject { + md := block.Metadata.GetMetadata(x, y, z) + displayText := decodeUText(md["utext"]) + + o := mapobjectdb.NewMapObject(mbpos, x, y, z, "sign") + o.Attributes["display_text"] = displayText + o.Attributes["material"] = this.Material + + return o +} diff --git a/mapobject/setup.go b/mapobject/setup.go index 831e7f70..6e504f59 100644 --- a/mapobject/setup.go +++ b/mapobject/setup.go @@ -190,61 +190,82 @@ func Setup(ctx *app.App) { if ctx.Config.MapObjects.Signs { l.AddMapObject("default:sign_wall_wood", &SignBlock{Material: "wood"}) l.AddMapObject("default:sign_wall_steel", &SignBlock{Material: "steel"}) - l.AddMapObject("mcl_signs:wall_sign", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_sprucewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_sprucewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_sprucewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_sprucewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_sprucewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_darkwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_darkwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_darkwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_darkwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_darkwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_junglewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_junglewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_junglewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_junglewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_junglewood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_acaciawood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_acaciawood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_acaciawood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_acaciawood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_acaciawood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_mangrove_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_mangrove_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_mangrove_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_mangrove_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_mangrove_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_warped_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_warped_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_warped_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_warped_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_warped_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_crimson_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_crimson_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_crimson_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_crimson_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_crimson_hyphae_wood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_bamboo", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_bamboo", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_bamboo", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_bamboo", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_bamboo", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_birchwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_birchwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_birchwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_birchwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_birchwood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:wall_sign_cherrywood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign_cherrywood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign22_5_cherrywood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign45_cherrywood", &SignBlock{Material: "wood"}) - l.AddMapObject("mcl_signs:standing_sign67_5_cherrywood", &SignBlock{Material: "wood"}) + // Mineclonia legacy + l.AddMapObject("mcl_signs:wall_sign", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_sprucewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_sprucewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_sprucewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_sprucewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_sprucewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_darkwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_darkwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_darkwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_darkwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_darkwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_junglewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_junglewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_junglewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_junglewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_junglewood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_acaciawood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_acaciawood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_acaciawood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_acaciawood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_acaciawood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_mangrove_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_mangrove_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_mangrove_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_mangrove_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_mangrove_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_warped_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_warped_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_warped_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_warped_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_warped_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_crimson_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_crimson_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_crimson_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_crimson_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_crimson_hyphae_wood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_bamboo", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_bamboo", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_bamboo", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_bamboo", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_bamboo", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_birchwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_birchwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_birchwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_birchwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_birchwood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:wall_sign_cherrywood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign_cherrywood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign22_5_cherrywood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign45_cherrywood", &MclSignBlock{Material: "wood"}) + l.AddMapObject("mcl_signs:standing_sign67_5_cherrywood", &MclSignBlock{Material: "wood"}) + // Mineclonia current + prefix := "mcl_signs:" + materials := []string{ + "acacia", "bamboo", "birch", "cherry_blossom", "crimson", + "dark_oak", "jungle", "mangrove", "oak", "pale_oak", + "spruce", "warped", + } + types := []string{ + "hanging_sign", + "hanging_sign_attached", + "hanging_sign_wall", + "standing_sign", + "wall_sign", + } + for _, t := range types { + for _, m := range materials { + blockName := prefix + t + "_" + m + l.AddMapObject(blockName, &MclSignBlock{Material: "wood"}) + } + } } //Phonograph diff --git a/public/js/map/Overlaysetup.js b/public/js/map/Overlaysetup.js index 7fad4155..8a851a4b 100644 --- a/public/js/map/Overlaysetup.js +++ b/public/js/map/Overlaysetup.js @@ -4,6 +4,7 @@ import ShopOverlay from './overlays/ShopOverlay.js'; import LabelOverlay from './overlays/LabelOverlay.js'; import TrainlineOverlay from './overlays/TrainlineOverlay.js'; import TravelnetOverlay from './overlays/TravelnetOverlay.js'; +import RespawnOverlay from './overlays/RespawnOverlay.js'; import BonesOverlay from './overlays/BonesOverlay.js'; import LcdOverlay from './overlays/LcdOverlay.js'; import DigitermOverlay from './overlays/DigitermOverlay.js'; @@ -81,6 +82,13 @@ export default function(cfg, map, overlays, wsChannel){ } } + if (cfg.mapobjects.respawn_places) { + overlays["Respawn places"] = new RespawnOverlay(cfg); + if (isDefault("respawn_places")) { + map.addLayer(overlays.RespawnPlaces); + } + } + if (cfg.mapobjects.bones) { overlays.Bones = new BonesOverlay(); if (isDefault("bones")) { diff --git a/public/js/map/overlays/RespawnOverlay.js b/public/js/map/overlays/RespawnOverlay.js new file mode 100644 index 00000000..dfc9093b --- /dev/null +++ b/public/js/map/overlays/RespawnOverlay.js @@ -0,0 +1,119 @@ +import debounce from '../../util/debounce.js'; +import layerMgr from '../../LayerManager.js'; + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +const get_places = async () => { + const response = await fetch("api/respawnplaces", { + headers: { + "Accept": "application/json" + } + }); + + if (!response.ok) { + throw new Error( + `Unable to load respawn places: ${response.status} ${response.statusText}` + ); + } + const places = await response.json(); + return places || {}; +}; + +export default L.LayerGroup.extend({ + initialize: function(cfg) { + L.LayerGroup.prototype.initialize.call(this); + this.placescolor = "green"; + if (cfg.respawn.placescolor) + this.placescolor = cfg.respawn.placescolor; + this.currentObjects = {}; + this.reDraw = this.reDraw.bind(this); + this.onMapMove = debounce(this.onMapMove.bind(this), 50); + // no onMinetestUpdate needed, like AirUtilsPlanesOverlay.js. Places do not change very often. + }, + + createPopup: function(place, name) { + if (!name) name = "unnamed"; + let html = "" + escapeHtml(name) + ""; + if (place.full_name) + html += "
" + escapeHtml(place.full_name); + return html; + }, + + getIcon: function(color){ + return L.AwesomeMarkers.icon({ + // person-arrow-up-from-line is my first choice, but not present + // in the bundled fontawesome file. + icon: "user-plus", + prefix: "fa", + markerColor: color || this.placescolor || "green" + }) + }, + + createMarker: function(place, name) { + let marker = L.marker([place.pos.z + 0.5, place.pos.x + 0.5]); + marker.bindPopup(this.createPopup(place, name)); + marker.setIcon(this.getIcon()); + return marker; + }, + + isInCurrentLayer: function(plane) { + let mapLayer = layerMgr.getCurrentLayer(); + + return ( + plane.pos.y >= (mapLayer.from*16) && + plane.pos.y <= ((mapLayer.to*16) + 15) + ); + }, + + getMaxDisplayedZoom: function(){ + return 5; + }, + + reDraw: async function() { + if (this.map.getZoom() < this.getMaxDisplayedZoom()) { + this.currentObjects = {}; + this.clearLayers(); + return; + } + + const places = await get_places(); + this.currentObjects = {}; + this.clearLayers(); + + Object.entries(places).forEach(([name,place]) => { + if (!this.isInCurrentLayer(place)) { + // not in current layer + return; + } + let marker = this.createMarker(place, name); + marker.addTo(this); + this.currentObjects[name] = marker; + }); + }, + + onMapMove: function(){ + this.reDraw(false); + }, + + onLayerChange: function(/*layer*/){ + this.reDraw(true); + }, + + onAdd: function(map) { + this.map = map; + map.on("zoomend", this.onMapMove); + this.reDraw(); + }, + + onRemove: function(map) { + map.off("zoomend", this.onMapMove); + this.clearLayers(); + } +}); diff --git a/readme.md b/readme.md index 26a5ff14..b475c04a 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,7 @@ See: [Incremental rendering](doc/incrementalrendering.md) * Protector display * LCD Displays as markers * Monitoring with [Prometheus](doc/prometheus.md) +* Mod [respawn](https://gitlab.com/cronvel/mt-respawn) places ## Planned Features @@ -121,4 +122,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/respawnparser/respawnparser.go b/respawnparser/respawnparser.go new file mode 100644 index 00000000..d99024d6 --- /dev/null +++ b/respawnparser/respawnparser.go @@ -0,0 +1,61 @@ +package respawnparser +// ref https://github.com/minetest-go/areasparser/blob/master/parser.go +// duck.ai +// docker go cache help: https://oneuptime.com/blog/post/2026-02-08-how-to-speed-up-docker-build-for-go-projects/view + +import ( + "encoding/json" + "fmt" + "io/ioutil" +) + +type File map[string]RespawnPlace + +type RespawnPlace struct { + FullName string `json:"full_name,omitempty"` + Pos GenericPos `json:"pos"` + Look *Direction `json:"look,omitempty"` +} + +type GenericPos struct { + X int `json:"x"` + Y int `json:"y"` + Z int `json:"z"` +} + +type Direction struct { + H float64 `json:"h"` + V float64 `json:"v"` +} + +func getInt(o interface{}) int { + v, _ := o.(float64) + return int(v) +} + +func (pos *GenericPos) UnmarshalJSON(data []byte) error { + m := make(map[string]interface{}) + err := json.Unmarshal(data, &m) + if err != nil { + return err + } + pos.X = getInt(m["x"]) + pos.Y = getInt(m["y"]) + pos.Z = getInt(m["z"]) + // do not cast to int like https://github.com/minetest-go/areasparser/blob/master/parser.go + return nil +} + +func ParseFile(filename string) (File, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var respawnplaces File + if err := json.Unmarshal(data, &respawnplaces); err != nil { + return nil, fmt.Errorf("parse %s: %w", filename, err) + } + + return respawnplaces, nil +} diff --git a/web/config.go b/web/config.go index 966fadb9..9c384a19 100644 --- a/web/config.go +++ b/web/config.go @@ -15,6 +15,7 @@ type PublicConfig struct { DefaultOverlays []string `json:"defaultoverlays"` PageName string `json:"pagename"` EnableSearch bool `json:"enablesearch"` + Respawn *app.RespawnConfig `json:"respawn"` } func (api *Api) GetConfig(resp http.ResponseWriter, req *http.Request) { @@ -27,6 +28,7 @@ func (api *Api) GetConfig(resp http.ResponseWriter, req *http.Request) { webcfg.DefaultOverlays = api.Context.Config.DefaultOverlays webcfg.PageName = api.Context.Config.PageName webcfg.EnableSearch = api.Context.Config.EnableSearch + webcfg.Respawn = api.Context.Config.Respawn json.NewEncoder(resp).Encode(webcfg) } diff --git a/web/respawn.go b/web/respawn.go new file mode 100644 index 00000000..483abeae --- /dev/null +++ b/web/respawn.go @@ -0,0 +1,57 @@ +package web + +import ( + "encoding/json" + "net/http" + + "os" + "sync" + "time" + + "mapserver/app" + "mapserver/respawnparser" +) + +type RespawnPlacesHandler struct { + ctx *app.App + cache map[string]respawnparser.RespawnPlace + lasttime int64 +} + +var mutex_respawn = &sync.Mutex{} + +const RESPAWN_PLACES_FILENAME = "places.respawn.db" + +func (h *RespawnPlacesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + info, err := os.Stat(RESPAWN_PLACES_FILENAME) + if info == nil || err != nil { + // no places file + resp.Header().Add("content-type", "application/json") + resp.Write([]byte("[]")) + return + } + + now := time.Now().Unix() + diff := now - h.lasttime + + if diff > 5 { + mutex_respawn.Lock() + + h.lasttime = now + places, err := respawnparser.ParseFile(RESPAWN_PLACES_FILENAME) + + if err != nil { + resp.WriteHeader(500) + resp.Write([]byte(err.Error())) + return + } + + h.cache = places + + mutex_respawn.Unlock() + } + + resp.Header().Add("content-type", "application/json") + json.NewEncoder(resp).Encode(h.cache) + +} diff --git a/web/serve.go b/web/serve.go index 90f3ac54..39da95c1 100644 --- a/web/serve.go +++ b/web/serve.go @@ -48,6 +48,10 @@ func Serve(ctx *app.App) { mux.Handle("/api/areas", &AreasHandler{ctx: ctx}) } + if ctx.Config.MapObjects.RespawnPlaces { + mux.Handle("/api/respawnplaces", &RespawnPlacesHandler{ctx: ctx}) + } + if ctx.Config.EnablePrometheus { mux.Handle("/metrics", promhttp.Handler()) }