├── .gitignore ├── .vscode └── tasks.json ├── README.md ├── WebMap ├── Config.cs ├── JSONParser.cs ├── MapDataServer.cs ├── WebMap.cs ├── WebMap.csproj ├── config.json ├── web-src │ ├── constants.js │ ├── index.js │ ├── map.js │ ├── onPointers.js │ ├── players.js │ ├── ui.js │ └── websocket.js └── web │ ├── index.html │ ├── mapIcons.png │ └── style.css ├── libs ├── 0Harmony.dll ├── BepInEx.Harmony.dll ├── BepInEx.dll └── websocket-sharp.dll ├── manifest.json ├── package-lock.json ├── package.json └── thumb.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | 331 | notes.txt 332 | ValheimWebMap.zip 333 | */web/main.js 334 | libs/UnityEngine* 335 | assembly_valheim.dll 336 | assembly_utils.dll 337 | WebMap.dll 338 | WebMap.zip 339 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | "WebMap/WebMap.csproj", 13 | "/property:GenerateFullPaths=true", 14 | "/consoleloggerparameters:NoSummary" 15 | ], 16 | "group": "build", 17 | "presentation": { 18 | "reveal": "silent" 19 | }, 20 | "problemMatcher": "$msCompile" 21 | }, 22 | { 23 | "label": "buildRelease", 24 | "command": "dotnet", 25 | "type": "shell", 26 | "args": [ 27 | "build", 28 | "WebMap/WebMap.csproj", 29 | "--configuration=Release", 30 | "/property:GenerateFullPaths=true", 31 | "/consoleloggerparameters:NoSummary" 32 | ], 33 | "group": "build", 34 | "presentation": { 35 | "reveal": "silent" 36 | }, 37 | "problemMatcher": "$msCompile" 38 | }, 39 | { 40 | "label": "copyDll", 41 | "type": "shell", 42 | "command": "cp ./WebMap/bin/Debug/WebMap.dll '/Program Files (x86)/Steam/steamapps/common/Valheim dedicated server/BepInEx/plugins/WebMap'", 43 | }, 44 | { 45 | "label": "copyWebDir", 46 | "type": "shell", 47 | "command": "cp -r ./WebMap/web/* '/Program Files (x86)/Steam/steamapps/common/Valheim dedicated server/BepInEx/plugins/WebMap/web'", 48 | }, 49 | { 50 | "label": "buildAndCopy", 51 | "dependsOn": [ 52 | "build", 53 | "copyDll", 54 | "copyWebDir" 55 | ], 56 | "dependsOrder": "sequence", 57 | "problemMatcher": [] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Valheim WebMap 2 | 3 | This server side mod creates a web based map that shows live players and allows shared exploration. After port forwarding the correct port, you can share `http://your_ip:port` to anyone else and they can see the map too. **Clients do not need to have any mods installed!** 4 | 5 | For players to show up on the map, they must set `visible to other players` in the in-game map screen. 6 | 7 | This has only been tested on a Valheim dedicated server. I'm not sure this will work with the built in server inclued with the base game. 8 | 9 | ## Features 10 | 11 | * A explorable map of your Valheim world in your browser that you can zoom with the mousewheel or pinch zoom on mobile. 12 | * Players can place their own pins with chat commands (see below for more info) 13 | * Map pings from in game players will show up on the web map as well. 14 | * Connected players list. 15 | * Auto follow player feature. 16 | 17 | ## Installation 18 | 19 | Place the WebMap directory in: 20 | 21 | `Steam\steamapps\common\Valheim dedicated server\BepInEx\plugins\WebMap` 22 | 23 | Optionally, open `WebMap/config.json` with a text editor and change stuff in there. 24 | 25 | ## Updating 26 | 27 | If you are updating, one additional thing you and anyone else using the web map might need to do is clear your browser cache. 28 | 29 | You may also be able to hold down the `shift` key and click the reload button in your browser. 30 | 31 | ## Chat Commands 32 | 33 | This mod supports placing pins with chat commands. Press `Enter` to start chatting in game. The commands are as follows: 34 | 35 | * `/pin` - Place a "dot" pin with no text on the map where you are currently standing. 36 | * `/pin my pin name` - Place a "dot" pin with "my pin name" under it on the map where you are currently standing. 37 | * `/pin [pin-type] [text]` - Place a pin of a certain type with optional text under it on the map where you are currently standing. 38 | * Pin types are: `dot`, `fire`, `mine`, `house` and `cave`. Example command: `/pin house my awesome base` 39 | * `/undoPin` - Delete your most recent pin. 40 | * `/deletePin [text]` - Delete the most recent pin that matches the text exactly. 41 | 42 | If a player creates too many pins, their oldest pin will be removed. There is a setting to control how many pins a player can create in `config.json`. 43 | 44 | ## Development 45 | 46 | To get your environment working, you will need to find and place these .dll files in the WebMap/libs directory. These dlls are usually found in: 47 | 48 | `Steam\steamapps\common\Valheim dedicated server\valheim_server_Data\Managed` 49 | * assembly_valheim.dll 50 | * UnityEngine.CoreModule.dll 51 | * UnityEngine.dll 52 | * UnityEngine.ImageConversionModule.dll (This one might be harder to find. Try googling.) 53 | * UnityEngine.UI.dll 54 | 55 | To get the fontend part building, you'll need node installed. After, just do `npm install` to install webpack. Then you can run `npm run build` to build `main.js` which should be included with the other web resources in `WebMap/web`. 56 | 57 | I wanted to get this working as soon as possible, so apollogies for messy code. 58 | 59 | ## Licence 60 | 61 | When applicable, assume stuff is under the MIT licence ( https://opensource.org/licenses/MIT ) 62 | I am not liable for any damages and there is no warranty etc... 63 | -------------------------------------------------------------------------------- /WebMap/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using TinyJson; 6 | 7 | using UnityEngine; 8 | 9 | namespace WebMap { 10 | 11 | static class WebMapConfig { 12 | 13 | public static int TEXTURE_SIZE = 2048; 14 | public static int PIXEL_SIZE = 12; 15 | public static float EXPLORE_RADIUS = 100f; 16 | public static float UPDATE_FOG_TEXTURE_INTERVAL = 1f; 17 | public static float SAVE_FOG_TEXTURE_INTERVAL = 30f; 18 | public static int MAX_PINS_PER_USER = 50; 19 | 20 | public static int SERVER_PORT = 3000; 21 | public static double PLAYER_UPDATE_INTERVAL = 0.5; 22 | public static bool CACHE_SERVER_FILES = false; 23 | 24 | public static string WORLD_NAME = ""; 25 | public static Vector3 WORLD_START_POS = Vector3.zero; 26 | public static int DEFAULT_ZOOM = 200; 27 | 28 | public static TValue GetValueOrDefault( 29 | this IDictionary dictionary, TKey key, TValue defaultValue) { 30 | 31 | TValue value; 32 | return dictionary.TryGetValue(key, out value) ? value : defaultValue; 33 | } 34 | 35 | public static void readConfigFile(string configFile) { 36 | string fileJson = ""; 37 | try { 38 | fileJson = File.ReadAllText(configFile); 39 | } catch { 40 | System.Console.WriteLine("~~~ WebMap: FAILED TO READ CONFIG FILE AT: " + configFile); 41 | System.Environment.Exit(1); 42 | } 43 | 44 | var configJson = (Dictionary)fileJson.FromJson(); 45 | 46 | if (configJson == null) { 47 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE CONFIG FILE AT: " + configFile + " . INVALID SYNTAX?"); 48 | System.Environment.Exit(1); 49 | } 50 | 51 | try { 52 | TEXTURE_SIZE = (int)configJson.GetValueOrDefault("texture_size", 2048); 53 | } catch { 54 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE texture_size VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 55 | } 56 | 57 | try { 58 | PIXEL_SIZE = (int)configJson.GetValueOrDefault("pixel_size", 12); 59 | } catch { 60 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE pixel_size VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 61 | } 62 | 63 | try { 64 | EXPLORE_RADIUS = (float)Convert.ToDouble(configJson.GetValueOrDefault("explore_radius", 100f)); 65 | } catch { 66 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE explore_radius VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 67 | } 68 | 69 | try { 70 | UPDATE_FOG_TEXTURE_INTERVAL = (float)Convert.ToDouble(configJson.GetValueOrDefault("update_fog_texture_interval", 1f)); 71 | } catch { 72 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE update_fog_texture_interval VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 73 | } 74 | 75 | try { 76 | SAVE_FOG_TEXTURE_INTERVAL = (float)Convert.ToDouble(configJson.GetValueOrDefault("save_fog_texture_interval", 30f)); 77 | } catch { 78 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE save_fog_texture_interval VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 79 | } 80 | 81 | try { 82 | MAX_PINS_PER_USER = (int)configJson.GetValueOrDefault("max_pins_per_user", 50); 83 | } catch { 84 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE max_pins_per_user VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 85 | } 86 | 87 | try { 88 | SERVER_PORT = (int)configJson.GetValueOrDefault("server_port", 3000); 89 | } catch { 90 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE server_port VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 91 | } 92 | 93 | try { 94 | PLAYER_UPDATE_INTERVAL = Convert.ToDouble(configJson.GetValueOrDefault("player_update_interval", 0.5)); 95 | } catch { 96 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE player_update_interval VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 97 | } 98 | 99 | try { 100 | CACHE_SERVER_FILES = (bool)configJson.GetValueOrDefault("cache_server_files", true); 101 | } catch { 102 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE cache_server_files VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 103 | } 104 | 105 | try { 106 | DEFAULT_ZOOM = (int)configJson.GetValueOrDefault("default_zoom", 200); 107 | } catch { 108 | System.Console.WriteLine("~~~ WebMap: FAILED TO PARSE default_zoom VALUE IN CONFIG FILE AT: " + configFile + " . INVALID TYPE?"); 109 | } 110 | } 111 | 112 | public static string getWorldName() { 113 | if (WORLD_NAME != "") { 114 | return WORLD_NAME; 115 | } 116 | string[] arguments = Environment.GetCommandLineArgs(); 117 | var worldName = ""; 118 | for (var t = 0; t < arguments.Length; t++) { 119 | if (arguments[t] == "-world") { 120 | worldName = arguments[t + 1]; 121 | break; 122 | } 123 | } 124 | WORLD_NAME = worldName; 125 | return worldName; 126 | } 127 | 128 | private static System.Globalization.CultureInfo culture = System.Globalization.CultureInfo.InvariantCulture; 129 | public static string str(int n) { 130 | return n.ToString(culture); 131 | } 132 | public static string str(float n, int precision = 2) { 133 | return n.ToString("F" + precision, culture); 134 | } 135 | public static string str(double n, int precision = 2) { 136 | return n.ToString("F" + precision, culture); 137 | } 138 | public static string str(long n) { 139 | return n.ToString(culture); 140 | } 141 | 142 | public static string makeClientConfigJSON() { 143 | var sb = new StringBuilder(); 144 | sb.Length = 0; 145 | 146 | sb.Append("{"); 147 | sb.Append($"\"world_name\":\"{getWorldName()}\","); 148 | sb.Append($"\"world_start_pos\": \"{str(WORLD_START_POS.x)},{str(WORLD_START_POS.y)},{str(WORLD_START_POS.z)}\","); 149 | sb.Append($"\"default_zoom\":{str(DEFAULT_ZOOM)},"); 150 | sb.Append($"\"texture_size\":{str(TEXTURE_SIZE)},"); 151 | sb.Append($"\"pixel_size\":{str(PIXEL_SIZE)},"); 152 | sb.Append($"\"update_interval\":{str(PLAYER_UPDATE_INTERVAL)},"); 153 | sb.Append($"\"explore_radius\":{str(EXPLORE_RADIUS)}"); 154 | sb.Append("}"); 155 | 156 | return sb.ToString(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /WebMap/JSONParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Runtime.Serialization; 6 | using System.Text; 7 | 8 | // https://github.com/zanders3/json 9 | 10 | namespace TinyJson 11 | { 12 | // Really simple JSON parser in ~300 lines 13 | // - Attempts to parse JSON files with minimal GC allocation 14 | // - Nice and simple "[1,2,3]".FromJson>() API 15 | // - Classes and structs can be parsed too! 16 | // class Foo { public int Value; } 17 | // "{\"Value\":10}".FromJson() 18 | // - Can parse JSON without type information into Dictionary and List e.g. 19 | // "[1,2,3]".FromJson().GetType() == typeof(List) 20 | // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) 21 | // - No JIT Emit support to support AOT compilation on iOS 22 | // - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead. 23 | // - Only public fields and property setters on classes/structs will be written to 24 | // 25 | // Limitations: 26 | // - No JIT Emit support to parse structures quickly 27 | // - Limited to parsing <2GB JSON files (due to int.MaxValue) 28 | // - Parsing of abstract classes or interfaces is NOT supported and will throw an exception. 29 | public static class JSONParser 30 | { 31 | [ThreadStatic] static Stack> splitArrayPool; 32 | [ThreadStatic] static StringBuilder stringBuilder; 33 | [ThreadStatic] static Dictionary> fieldInfoCache; 34 | [ThreadStatic] static Dictionary> propertyInfoCache; 35 | 36 | public static T FromJson(this string json) 37 | { 38 | // Initialize, if needed, the ThreadStatic variables 39 | if (propertyInfoCache == null) propertyInfoCache = new Dictionary>(); 40 | if (fieldInfoCache == null) fieldInfoCache = new Dictionary>(); 41 | if (stringBuilder == null) stringBuilder = new StringBuilder(); 42 | if (splitArrayPool == null) splitArrayPool = new Stack>(); 43 | 44 | //Remove all whitespace not within strings to make parsing simpler 45 | stringBuilder.Length = 0; 46 | for (int i = 0; i < json.Length; i++) 47 | { 48 | char c = json[i]; 49 | if (c == '"') 50 | { 51 | i = AppendUntilStringEnd(true, i, json); 52 | continue; 53 | } 54 | if (char.IsWhiteSpace(c)) 55 | continue; 56 | 57 | stringBuilder.Append(c); 58 | } 59 | 60 | //Parse the thing! 61 | return (T)ParseValue(typeof(T), stringBuilder.ToString()); 62 | } 63 | 64 | static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json) 65 | { 66 | stringBuilder.Append(json[startIdx]); 67 | for (int i = startIdx + 1; i < json.Length; i++) 68 | { 69 | if (json[i] == '\\') 70 | { 71 | if (appendEscapeCharacter) 72 | stringBuilder.Append(json[i]); 73 | stringBuilder.Append(json[i + 1]); 74 | i++;//Skip next character as it is escaped 75 | } 76 | else if (json[i] == '"') 77 | { 78 | stringBuilder.Append(json[i]); 79 | return i; 80 | } 81 | else 82 | stringBuilder.Append(json[i]); 83 | } 84 | return json.Length - 1; 85 | } 86 | 87 | //Splits { :, : } and [ , ] into a list of strings 88 | static List Split(string json) 89 | { 90 | List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List(); 91 | splitArray.Clear(); 92 | if (json.Length == 2) 93 | return splitArray; 94 | int parseDepth = 0; 95 | stringBuilder.Length = 0; 96 | for (int i = 1; i < json.Length - 1; i++) 97 | { 98 | switch (json[i]) 99 | { 100 | case '[': 101 | case '{': 102 | parseDepth++; 103 | break; 104 | case ']': 105 | case '}': 106 | parseDepth--; 107 | break; 108 | case '"': 109 | i = AppendUntilStringEnd(true, i, json); 110 | continue; 111 | case ',': 112 | case ':': 113 | if (parseDepth == 0) 114 | { 115 | splitArray.Add(stringBuilder.ToString()); 116 | stringBuilder.Length = 0; 117 | continue; 118 | } 119 | break; 120 | } 121 | 122 | stringBuilder.Append(json[i]); 123 | } 124 | 125 | splitArray.Add(stringBuilder.ToString()); 126 | 127 | return splitArray; 128 | } 129 | 130 | internal static object ParseValue(Type type, string json) 131 | { 132 | if (type == typeof(string)) 133 | { 134 | if (json.Length <= 2) 135 | return string.Empty; 136 | StringBuilder parseStringBuilder = new StringBuilder(json.Length); 137 | for (int i = 1; i < json.Length - 1; ++i) 138 | { 139 | if (json[i] == '\\' && i + 1 < json.Length - 1) 140 | { 141 | int j = "\"\\nrtbf/".IndexOf(json[i + 1]); 142 | if (j >= 0) 143 | { 144 | parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]); 145 | ++i; 146 | continue; 147 | } 148 | if (json[i + 1] == 'u' && i + 5 < json.Length - 1) 149 | { 150 | UInt32 c = 0; 151 | if (UInt32.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c)) 152 | { 153 | parseStringBuilder.Append((char)c); 154 | i += 5; 155 | continue; 156 | } 157 | } 158 | } 159 | parseStringBuilder.Append(json[i]); 160 | } 161 | return parseStringBuilder.ToString(); 162 | } 163 | if (type.IsPrimitive) 164 | { 165 | var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture); 166 | return result; 167 | } 168 | if (type == typeof(decimal)) 169 | { 170 | decimal result; 171 | decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); 172 | return result; 173 | } 174 | if (json == "null") 175 | { 176 | return null; 177 | } 178 | if (type.IsEnum) 179 | { 180 | if (json[0] == '"') 181 | json = json.Substring(1, json.Length - 2); 182 | try 183 | { 184 | return Enum.Parse(type, json, false); 185 | } 186 | catch 187 | { 188 | return 0; 189 | } 190 | } 191 | if (type.IsArray) 192 | { 193 | Type arrayType = type.GetElementType(); 194 | if (json[0] != '[' || json[json.Length - 1] != ']') 195 | return null; 196 | 197 | List elems = Split(json); 198 | Array newArray = Array.CreateInstance(arrayType, elems.Count); 199 | for (int i = 0; i < elems.Count; i++) 200 | newArray.SetValue(ParseValue(arrayType, elems[i]), i); 201 | splitArrayPool.Push(elems); 202 | return newArray; 203 | } 204 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) 205 | { 206 | Type listType = type.GetGenericArguments()[0]; 207 | if (json[0] != '[' || json[json.Length - 1] != ']') 208 | return null; 209 | 210 | List elems = Split(json); 211 | var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count }); 212 | for (int i = 0; i < elems.Count; i++) 213 | list.Add(ParseValue(listType, elems[i])); 214 | splitArrayPool.Push(elems); 215 | return list; 216 | } 217 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) 218 | { 219 | Type keyType, valueType; 220 | { 221 | Type[] args = type.GetGenericArguments(); 222 | keyType = args[0]; 223 | valueType = args[1]; 224 | } 225 | 226 | //Refuse to parse dictionary keys that aren't of type string 227 | if (keyType != typeof(string)) 228 | return null; 229 | //Must be a valid dictionary element 230 | if (json[0] != '{' || json[json.Length - 1] != '}') 231 | return null; 232 | //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON 233 | List elems = Split(json); 234 | if (elems.Count % 2 != 0) 235 | return null; 236 | 237 | var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 }); 238 | for (int i = 0; i < elems.Count; i += 2) 239 | { 240 | if (elems[i].Length <= 2) 241 | continue; 242 | string keyValue = elems[i].Substring(1, elems[i].Length - 2); 243 | object val = ParseValue(valueType, elems[i + 1]); 244 | dictionary[keyValue] = val; 245 | } 246 | return dictionary; 247 | } 248 | if (type == typeof(object)) 249 | { 250 | return ParseAnonymousValue(json); 251 | } 252 | if (json[0] == '{' && json[json.Length - 1] == '}') 253 | { 254 | return ParseObject(type, json); 255 | } 256 | 257 | return null; 258 | } 259 | 260 | static object ParseAnonymousValue(string json) 261 | { 262 | if (json.Length == 0) 263 | return null; 264 | if (json[0] == '{' && json[json.Length - 1] == '}') 265 | { 266 | List elems = Split(json); 267 | if (elems.Count % 2 != 0) 268 | return null; 269 | var dict = new Dictionary(elems.Count / 2); 270 | for (int i = 0; i < elems.Count; i += 2) 271 | dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]); 272 | return dict; 273 | } 274 | if (json[0] == '[' && json[json.Length - 1] == ']') 275 | { 276 | List items = Split(json); 277 | var finalList = new List(items.Count); 278 | for (int i = 0; i < items.Count; i++) 279 | finalList.Add(ParseAnonymousValue(items[i])); 280 | return finalList; 281 | } 282 | if (json[0] == '"' && json[json.Length - 1] == '"') 283 | { 284 | string str = json.Substring(1, json.Length - 2); 285 | return str.Replace("\\", string.Empty); 286 | } 287 | if (char.IsDigit(json[0]) || json[0] == '-') 288 | { 289 | if (json.Contains(".")) 290 | { 291 | double result; 292 | double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result); 293 | return result; 294 | } 295 | else 296 | { 297 | int result; 298 | int.TryParse(json, out result); 299 | return result; 300 | } 301 | } 302 | if (json == "true") 303 | return true; 304 | if (json == "false") 305 | return false; 306 | // handles json == "null" as well as invalid JSON 307 | return null; 308 | } 309 | 310 | static Dictionary CreateMemberNameDictionary(T[] members) where T : MemberInfo 311 | { 312 | Dictionary nameToMember = new Dictionary(StringComparer.OrdinalIgnoreCase); 313 | for (int i = 0; i < members.Length; i++) 314 | { 315 | T member = members[i]; 316 | if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true)) 317 | continue; 318 | 319 | string name = member.Name; 320 | if (member.IsDefined(typeof(DataMemberAttribute), true)) 321 | { 322 | DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true); 323 | if (!string.IsNullOrEmpty(dataMemberAttribute.Name)) 324 | name = dataMemberAttribute.Name; 325 | } 326 | 327 | nameToMember.Add(name, member); 328 | } 329 | 330 | return nameToMember; 331 | } 332 | 333 | static object ParseObject(Type type, string json) 334 | { 335 | object instance = FormatterServices.GetUninitializedObject(type); 336 | 337 | //The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON 338 | List elems = Split(json); 339 | if (elems.Count % 2 != 0) 340 | return instance; 341 | 342 | Dictionary nameToField; 343 | Dictionary nameToProperty; 344 | if (!fieldInfoCache.TryGetValue(type, out nameToField)) 345 | { 346 | nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); 347 | fieldInfoCache.Add(type, nameToField); 348 | } 349 | if (!propertyInfoCache.TryGetValue(type, out nameToProperty)) 350 | { 351 | nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy)); 352 | propertyInfoCache.Add(type, nameToProperty); 353 | } 354 | 355 | for (int i = 0; i < elems.Count; i += 2) 356 | { 357 | if (elems[i].Length <= 2) 358 | continue; 359 | string key = elems[i].Substring(1, elems[i].Length - 2); 360 | string value = elems[i + 1]; 361 | 362 | FieldInfo fieldInfo; 363 | PropertyInfo propertyInfo; 364 | if (nameToField.TryGetValue(key, out fieldInfo)) 365 | fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value)); 366 | else if (nameToProperty.TryGetValue(key, out propertyInfo)) 367 | propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null); 368 | } 369 | 370 | return instance; 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /WebMap/MapDataServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Reflection; 6 | 7 | using WebSocketSharp.Net; 8 | using WebSocketSharp.Server; 9 | 10 | using UnityEngine; 11 | using static WebMap.WebMapConfig; 12 | 13 | namespace WebMap { 14 | 15 | public class WebSocketHandler : WebSocketBehavior { 16 | public WebSocketHandler() {} 17 | // protected override void OnOpen() { 18 | // Context.WebSocket.Send("hi " + ID); 19 | // } 20 | 21 | // protected override void OnClose(CloseEventArgs e) { 22 | // } 23 | 24 | // protected override void OnMessage(MessageEventArgs e) { 25 | // Sessions.Broadcast(e.Data); 26 | // } 27 | } 28 | 29 | public class MapDataServer { 30 | private HttpServer httpServer; 31 | private string publicRoot; 32 | private Dictionary fileCache; 33 | private System.Threading.Timer broadcastTimer; 34 | private WebSocketServiceHost webSocketHandler; 35 | 36 | public byte[] mapImageData; 37 | public Texture2D fogTexture; 38 | public List players = new List(); 39 | public List pins = new List(); 40 | 41 | static Dictionary contentTypes = new Dictionary() { 42 | { "html", "text/html" }, 43 | { "js", "text/javascript" }, 44 | { "css", "text/css" }, 45 | { "png", "image/png" } 46 | }; 47 | 48 | public MapDataServer() { 49 | httpServer = new HttpServer(WebMapConfig.SERVER_PORT); 50 | httpServer.AddWebSocketService("/"); 51 | httpServer.KeepClean = true; 52 | 53 | webSocketHandler = httpServer.WebSocketServices["/"]; 54 | 55 | broadcastTimer = new System.Threading.Timer((e) => { 56 | var dataString = ""; 57 | players.ForEach(player => { 58 | ZDO zdoData = null; 59 | try { 60 | zdoData = ZDOMan.instance.GetZDO(player.m_characterID); 61 | } catch {} 62 | 63 | if (zdoData != null) { 64 | var pos = zdoData.GetPosition(); 65 | var maxHealth = zdoData.GetFloat("max_health", 25f); 66 | var health = zdoData.GetFloat("health", maxHealth); 67 | maxHealth = Mathf.Max(maxHealth, health); 68 | 69 | if (player.m_publicRefPos) { 70 | dataString += $"{player.m_uid}\n{player.m_playerName}\n{str(pos.x)},{str(pos.y)},{str(pos.z)}\n{str(health)}\n{str(maxHealth)}\n\n"; 71 | } else { 72 | dataString += $"{player.m_uid}\n{player.m_playerName}\nhidden\n\n"; 73 | } 74 | } 75 | }); 76 | if (dataString.Length > 0) { 77 | webSocketHandler.Sessions.Broadcast("players\n" + dataString.Trim()); 78 | } 79 | }, null, TimeSpan.Zero, TimeSpan.FromSeconds(WebMapConfig.PLAYER_UPDATE_INTERVAL)); 80 | 81 | publicRoot = Path.Combine(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "web"); 82 | 83 | fileCache = new Dictionary(); 84 | 85 | httpServer.OnGet += (sender, e) => { 86 | var req = e.Request; 87 | // Debug.Log("~~~ Got GET Request for: " + req.RawUrl); 88 | 89 | if (ProcessSpecialRoutes(e)) { 90 | return; 91 | } 92 | 93 | ServeStaticFiles(e); 94 | }; 95 | } 96 | 97 | public void Stop() { 98 | broadcastTimer.Dispose(); 99 | httpServer.Stop(); 100 | } 101 | 102 | private void ServeStaticFiles(HttpRequestEventArgs e) { 103 | var req = e.Request; 104 | var res = e.Response; 105 | 106 | var rawRequestPath = req.RawUrl; 107 | if (rawRequestPath == "/") { 108 | rawRequestPath = "/index.html"; 109 | } 110 | 111 | var pathParts = rawRequestPath.Split('/'); 112 | var requestedFile = pathParts[pathParts.Length - 1]; 113 | var fileParts = requestedFile.Split('.'); 114 | var fileExt = fileParts[fileParts.Length - 1]; 115 | 116 | if (contentTypes.ContainsKey(fileExt)) { 117 | byte[] requestedFileBytes = new byte[0]; 118 | if (fileCache.ContainsKey(requestedFile)) { 119 | requestedFileBytes = fileCache[requestedFile]; 120 | } else { 121 | var filePath = Path.Combine(publicRoot, requestedFile); 122 | try { 123 | requestedFileBytes = File.ReadAllBytes(filePath); 124 | if (WebMapConfig.CACHE_SERVER_FILES) { 125 | fileCache.Add(requestedFile, requestedFileBytes); 126 | } 127 | } catch (Exception ex) { 128 | Debug.Log("WebMap: FAILED TO READ FILE! " + ex.Message); 129 | } 130 | } 131 | 132 | if (requestedFileBytes.Length > 0) { 133 | res.Headers.Add(HttpResponseHeader.CacheControl, "public, max-age=604800, immutable"); 134 | res.ContentType = contentTypes[fileExt]; 135 | res.StatusCode = 200; 136 | res.ContentLength64 = requestedFileBytes.Length; 137 | res.Close(requestedFileBytes, true); 138 | } else { 139 | res.StatusCode = 404; 140 | res.Close(); 141 | } 142 | } else { 143 | res.StatusCode = 404; 144 | res.Close(); 145 | } 146 | } 147 | 148 | private bool ProcessSpecialRoutes(HttpRequestEventArgs e) { 149 | var req = e.Request; 150 | var res = e.Response; 151 | var rawRequestPath = req.RawUrl; 152 | byte[] textBytes; 153 | 154 | switch(rawRequestPath) { 155 | case "/config": 156 | res.Headers.Add(HttpResponseHeader.CacheControl, "no-cache"); 157 | res.ContentType = "application/json"; 158 | res.StatusCode = 200; 159 | textBytes = Encoding.UTF8.GetBytes(WebMapConfig.makeClientConfigJSON()); 160 | res.ContentLength64 = textBytes.Length; 161 | res.Close(textBytes, true); 162 | return true; 163 | case "/map": 164 | // Doing things this way to make the full map harder to accidentally see. 165 | res.Headers.Add(HttpResponseHeader.CacheControl, "public, max-age=604800, immutable"); 166 | res.ContentType = "application/octet-stream"; 167 | res.StatusCode = 200; 168 | res.ContentLength64 = mapImageData.Length; 169 | res.Close(mapImageData, true); 170 | return true; 171 | case "/fog": 172 | res.Headers.Add(HttpResponseHeader.CacheControl, "no-cache"); 173 | res.ContentType = "image/png"; 174 | res.StatusCode = 200; 175 | var fogBytes = fogTexture.EncodeToPNG(); 176 | res.ContentLength64 = fogBytes.Length; 177 | res.Close(fogBytes, true); 178 | return true; 179 | case "/pins": 180 | res.Headers.Add(HttpResponseHeader.CacheControl, "no-cache"); 181 | res.ContentType = "text/csv"; 182 | res.StatusCode = 200; 183 | var text = String.Join("\n", pins); 184 | textBytes = Encoding.UTF8.GetBytes(text); 185 | res.ContentLength64 = textBytes.Length; 186 | res.Close(textBytes, true); 187 | return true; 188 | } 189 | return false; 190 | } 191 | 192 | public void ListenAsync() { 193 | httpServer.Start(); 194 | 195 | if (httpServer.IsListening) { 196 | Debug.Log($"WebMap: HTTP Server Listening on port {WebMapConfig.SERVER_PORT}"); 197 | } else { 198 | Debug.Log("WebMap: HTTP Server Failed To Start !!!"); 199 | } 200 | } 201 | 202 | public void BroadcastPing(long id, string name, Vector3 position) { 203 | webSocketHandler.Sessions.Broadcast($"ping\n{str(id)}\n{name}\n{str(position.x)},{str(position.z)}"); 204 | } 205 | 206 | public void AddPin(string id, string pinId, string type, string name, Vector3 position, string pinText) { 207 | pins.Add($"{id},{pinId},{type},{name},{str(position.x)},{str(position.z)},{pinText}"); 208 | webSocketHandler.Sessions.Broadcast($"pin\n{id}\n{pinId}\n{type}\n{name}\n{str(position.x)},{str(position.z)}\n{pinText}"); 209 | } 210 | 211 | public void RemovePin(int idx) { 212 | var pin = pins[idx]; 213 | var pinParts = pin.Split(','); 214 | pins.RemoveAt(idx); 215 | webSocketHandler.Sessions.Broadcast($"rmpin\n{pinParts[1]}"); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /WebMap/WebMap.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text.RegularExpressions; 5 | using System.IO; 6 | using System.Reflection; 7 | using BepInEx; 8 | using UnityEngine; 9 | using HarmonyLib; 10 | using static ZRoutedRpc; 11 | 12 | namespace WebMap { 13 | //This attribute is required, and lists metadata for your plugin. 14 | //The GUID should be a unique ID for this plugin, which is human readable (as it is used in places like the config). I like to use the java package notation, which is "com.[your name here].[your plugin name here]" 15 | //The name is the name of the plugin that's displayed on load, and the version number just specifies what version the plugin is. 16 | [BepInPlugin("com.kylepaulsen.valheim.webmap", "WebMap", "1.2.0")] 17 | 18 | //This is the main declaration of our plugin class. BepInEx searches for all classes inheriting from BaseUnityPlugin to initialize on startup. 19 | //BaseUnityPlugin itself inherits from MonoBehaviour, so you can use this as a reference for what you can declare and use in your plugin class: https://docs.unity3d.com/ScriptReference/MonoBehaviour.html 20 | public class WebMap : BaseUnityPlugin { 21 | 22 | static readonly DateTime unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 23 | static readonly HashSet ALLOWED_PINS = new HashSet { "dot", "fire", "mine", "house", "cave" }; 24 | 25 | static MapDataServer mapDataServer; 26 | static string worldDataPath; 27 | 28 | private bool fogTextureNeedsSaving = false; 29 | 30 | //The Awake() method is run at the very start when the game is initialized. 31 | public void Awake() { 32 | var harmony = new Harmony("com.kylepaulsen.valheim.webmap"); 33 | Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), (string) null); 34 | 35 | var pluginPath = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 36 | WebMapConfig.readConfigFile(Path.Combine(pluginPath, "config.json")); 37 | 38 | var mapDataPath = Path.Combine(pluginPath, "map_data"); 39 | Directory.CreateDirectory(mapDataPath); 40 | worldDataPath = Path.Combine(mapDataPath, WebMapConfig.getWorldName()); 41 | Directory.CreateDirectory(worldDataPath); 42 | 43 | mapDataServer = new MapDataServer(); 44 | mapDataServer.ListenAsync(); 45 | 46 | var mapImagePath = Path.Combine(worldDataPath, "map"); 47 | try { 48 | mapDataServer.mapImageData = File.ReadAllBytes(mapImagePath); 49 | } catch (Exception e) { 50 | Debug.Log("WebMap: Failed to read map image data from disk. " + e.Message); 51 | } 52 | 53 | var fogImagePath = Path.Combine(worldDataPath, "fog.png"); 54 | try { 55 | var fogTexture = new Texture2D(WebMapConfig.TEXTURE_SIZE, WebMapConfig.TEXTURE_SIZE); 56 | var fogBytes = File.ReadAllBytes(fogImagePath); 57 | fogTexture.LoadImage(fogBytes); 58 | mapDataServer.fogTexture = fogTexture; 59 | } catch (Exception e) { 60 | Debug.Log("WebMap: Failed to read fog image data from disk... Making new fog image..." + e.Message); 61 | var fogTexture = new Texture2D(WebMapConfig.TEXTURE_SIZE, WebMapConfig.TEXTURE_SIZE, TextureFormat.RGB24, false); 62 | var fogColors = new Color32[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; 63 | for (var t = 0; t < fogColors.Length; t++) { 64 | fogColors[t] = Color.black; 65 | } 66 | fogTexture.SetPixels32(fogColors); 67 | var fogPngBytes = fogTexture.EncodeToPNG(); 68 | 69 | mapDataServer.fogTexture = fogTexture; 70 | try { 71 | File.WriteAllBytes(fogImagePath, fogPngBytes); 72 | } catch { 73 | Debug.Log("WebMap: FAILED TO WRITE FOG FILE!"); 74 | } 75 | } 76 | 77 | InvokeRepeating("UpdateFogTexture", WebMapConfig.UPDATE_FOG_TEXTURE_INTERVAL, WebMapConfig.UPDATE_FOG_TEXTURE_INTERVAL); 78 | InvokeRepeating("SaveFogTexture", WebMapConfig.SAVE_FOG_TEXTURE_INTERVAL, WebMapConfig.SAVE_FOG_TEXTURE_INTERVAL); 79 | 80 | var mapPinsFile = Path.Combine(worldDataPath, "pins.csv"); 81 | try { 82 | var pinsLines = File.ReadAllLines(mapPinsFile); 83 | mapDataServer.pins = new List(pinsLines); 84 | } catch (Exception e) { 85 | Debug.Log("WebMap: Failed to read pins.csv from disk. " + e.Message); 86 | } 87 | } 88 | 89 | public void UpdateFogTexture() { 90 | int pixelExploreRadius = (int)Mathf.Ceil(WebMapConfig.EXPLORE_RADIUS / WebMapConfig.PIXEL_SIZE); 91 | int pixelExploreRadiusSquared = pixelExploreRadius * pixelExploreRadius; 92 | var halfTextureSize = WebMapConfig.TEXTURE_SIZE / 2; 93 | 94 | mapDataServer.players.ForEach(player => { 95 | if (player.m_publicRefPos) { 96 | ZDO zdoData = null; 97 | try { 98 | zdoData = ZDOMan.instance.GetZDO(player.m_characterID); 99 | } catch {} 100 | if (zdoData != null) { 101 | var pos = zdoData.GetPosition(); 102 | var pixelX = Mathf.RoundToInt(pos.x / WebMapConfig.PIXEL_SIZE + halfTextureSize); 103 | var pixelY = Mathf.RoundToInt(pos.z / WebMapConfig.PIXEL_SIZE + halfTextureSize); 104 | for (var y = pixelY - pixelExploreRadius; y <= pixelY + pixelExploreRadius; y++) { 105 | for (var x = pixelX - pixelExploreRadius; x <= pixelX + pixelExploreRadius; x++) { 106 | if (y >= 0 && x >= 0 && y < WebMapConfig.TEXTURE_SIZE && x < WebMapConfig.TEXTURE_SIZE) { 107 | var xDiff = pixelX - x; 108 | var yDiff = pixelY - y; 109 | var currentExploreRadiusSquared = xDiff * xDiff + yDiff * yDiff; 110 | if (currentExploreRadiusSquared < pixelExploreRadiusSquared) { 111 | var fogTexColor = mapDataServer.fogTexture.GetPixel(x, y); 112 | if (fogTexColor.r < 1f) { 113 | fogTextureNeedsSaving = true; 114 | mapDataServer.fogTexture.SetPixel(x, y, Color.white); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | }); 123 | } 124 | 125 | public void SaveFogTexture() { 126 | if (mapDataServer.players.Count > 0 && fogTextureNeedsSaving) { 127 | byte[] pngBytes = mapDataServer.fogTexture.EncodeToPNG(); 128 | 129 | // Debug.Log("Saving fog file..."); 130 | try { 131 | File.WriteAllBytes(Path.Combine(worldDataPath, "fog.png"), pngBytes); 132 | fogTextureNeedsSaving = false; 133 | } catch { 134 | Debug.Log("WebMap: FAILED TO WRITE FOG FILE!"); 135 | } 136 | } 137 | } 138 | 139 | public static void SavePins() { 140 | var mapPinsFile = Path.Combine(worldDataPath, "pins.csv"); 141 | try { 142 | File.WriteAllLines(mapPinsFile, mapDataServer.pins); 143 | } catch { 144 | Debug.Log("WebMap: FAILED TO WRITE PINS FILE!"); 145 | } 146 | } 147 | 148 | [HarmonyPatch(typeof (ZoneSystem), "Start")] 149 | private class ZoneSystemPatch { 150 | 151 | static readonly Color DeepWaterColor = new Color(0.36105883f, 0.36105883f, 0.43137255f); 152 | static readonly Color ShallowWaterColor = new Color(0.574f, 0.50709206f, 0.47892025f); 153 | static readonly Color ShoreColor = new Color(0.1981132f, 0.12241901f, 0.1503943f); 154 | 155 | static Color GetMaskColor(float wx, float wy, float height, Heightmap.Biome biome) { 156 | var noForest = new Color(0f, 0f, 0f, 0f); 157 | var forest = new Color(1f, 0f, 0f, 0f); 158 | 159 | if (height < ZoneSystem.instance.m_waterLevel) { 160 | return noForest; 161 | } 162 | if (biome == Heightmap.Biome.Meadows) { 163 | if (!WorldGenerator.InForest(new Vector3(wx, 0f, wy))) { 164 | return noForest; 165 | } 166 | return forest; 167 | } else if (biome == Heightmap.Biome.Plains) { 168 | if (WorldGenerator.GetForestFactor(new Vector3(wx, 0f, wy)) >= 0.8f) { 169 | return noForest; 170 | } 171 | return forest; 172 | } else { 173 | if (biome == Heightmap.Biome.BlackForest || biome == Heightmap.Biome.Mistlands) { 174 | return forest; 175 | } 176 | return noForest; 177 | } 178 | } 179 | 180 | static Color GetPixelColor(Heightmap.Biome biome) { 181 | var m_meadowsColor = new Color(0.573f, 0.655f, 0.361f); 182 | var m_swampColor = new Color(0.639f, 0.447f, 0.345f); 183 | var m_mountainColor = new Color(1f, 1f, 1f); 184 | var m_blackforestColor = new Color(0.420f, 0.455f, 0.247f); 185 | var m_heathColor = new Color(0.906f, 0.671f, 0.470f); 186 | var m_ashlandsColor = new Color(0.690f, 0.192f, 0.192f); 187 | var m_deepnorthColor = new Color(1f, 1f, 1f); 188 | var m_mistlandsColor = new Color(0.325f, 0.325f, 0.325f); 189 | 190 | if (biome <= Heightmap.Biome.Plains) { 191 | switch (biome) { 192 | case Heightmap.Biome.Meadows: 193 | return m_meadowsColor; 194 | case Heightmap.Biome.Swamp: 195 | return m_swampColor; 196 | case (Heightmap.Biome)3: 197 | break; 198 | case Heightmap.Biome.Mountain: 199 | return m_mountainColor; 200 | default: 201 | if (biome == Heightmap.Biome.BlackForest) { 202 | return m_blackforestColor; 203 | } 204 | if (biome == Heightmap.Biome.Plains) { 205 | return m_heathColor; 206 | } 207 | break; 208 | } 209 | } else if (biome <= Heightmap.Biome.DeepNorth) { 210 | if (biome == Heightmap.Biome.AshLands) { 211 | return m_ashlandsColor; 212 | } 213 | if (biome == Heightmap.Biome.DeepNorth) { 214 | return m_deepnorthColor; 215 | } 216 | } else { 217 | if (biome == Heightmap.Biome.Ocean) { 218 | return Color.white; 219 | } 220 | if (biome == Heightmap.Biome.Mistlands) { 221 | return m_mistlandsColor; 222 | } 223 | } 224 | return Color.white; 225 | } 226 | 227 | static void Postfix(ZoneSystem __instance) { 228 | Vector3 startPos; 229 | ZoneSystem.instance.GetLocationIcon("StartTemple", out startPos); 230 | WebMapConfig.WORLD_START_POS = startPos; 231 | 232 | if (mapDataServer.mapImageData != null) { 233 | Debug.Log("WebMap: MAP ALREADY BUILT!"); 234 | return; 235 | } 236 | Debug.Log("WebMap: BUILD MAP!"); 237 | 238 | int num = WebMapConfig.TEXTURE_SIZE / 2; 239 | float num2 = WebMapConfig.PIXEL_SIZE / 2f; 240 | Color32[] colorArray = new Color32[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; 241 | Color32[] treeMaskArray = new Color32[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; 242 | float[] heightArray = new float[WebMapConfig.TEXTURE_SIZE * WebMapConfig.TEXTURE_SIZE]; 243 | for (int i = 0; i < WebMapConfig.TEXTURE_SIZE; i++) { 244 | for (int j = 0; j < WebMapConfig.TEXTURE_SIZE; j++) { 245 | float wx = (float)(j - num) * WebMapConfig.PIXEL_SIZE + num2; 246 | float wy = (float)(i - num) * WebMapConfig.PIXEL_SIZE + num2; 247 | Heightmap.Biome biome = WorldGenerator.instance.GetBiome(wx, wy); 248 | float biomeHeight = WorldGenerator.instance.GetBiomeHeight(biome, wx, wy); 249 | colorArray[i * WebMapConfig.TEXTURE_SIZE + j] = GetPixelColor(biome); 250 | treeMaskArray[i * WebMapConfig.TEXTURE_SIZE + j] = GetMaskColor(wx, wy, biomeHeight, biome); 251 | heightArray[i * WebMapConfig.TEXTURE_SIZE + j] = biomeHeight; 252 | } 253 | } 254 | 255 | var waterLevel = ZoneSystem.instance.m_waterLevel; 256 | var sunDir = new Vector3(-0.57735f, 0.57735f, 0.57735f); 257 | var newColors = new Color[colorArray.Length]; 258 | 259 | for (var t = 0; t < colorArray.Length; t++) { 260 | var h = heightArray[t]; 261 | 262 | var tUp = t - WebMapConfig.TEXTURE_SIZE; 263 | if (tUp < 0) { 264 | tUp = t; 265 | } 266 | var tDown = t + WebMapConfig.TEXTURE_SIZE; 267 | if (tDown > colorArray.Length - 1) { 268 | tDown = t; 269 | } 270 | var tRight = t + 1; 271 | if (tRight > colorArray.Length - 1) { 272 | tRight = t; 273 | } 274 | var tLeft = t - 1; 275 | if (tLeft < 0) { 276 | tLeft = t; 277 | } 278 | var hUp = heightArray[tUp]; 279 | var hRight = heightArray[tRight]; 280 | var hLeft = heightArray[tLeft]; 281 | var hDown = heightArray[tDown]; 282 | 283 | var va = new Vector3(2f, 0f, hRight - hLeft).normalized; 284 | var vb = new Vector3(0f, 2f, hUp - hDown).normalized; 285 | var normal = Vector3.Cross(va, vb); 286 | 287 | var surfaceLight = Vector3.Dot(normal, sunDir) * 0.25f + 0.75f; 288 | 289 | float shoreMask = Mathf.Clamp(h - waterLevel, 0, 1); 290 | float shallowRamp = Mathf.Clamp((h - waterLevel + 0.2f * 12.5f) * 0.5f, 0, 1); 291 | float deepRamp = Mathf.Clamp((h - waterLevel + 1f * 12.5f) * 0.1f, 0, 1); 292 | 293 | var mapColor = colorArray[t]; 294 | Color ans = Color.Lerp(ShoreColor, mapColor, shoreMask); 295 | ans = Color.Lerp(ShallowWaterColor, ans, shallowRamp); 296 | ans = Color.Lerp(DeepWaterColor, ans, deepRamp); 297 | 298 | newColors[t] = new Color(ans.r * surfaceLight, ans.g * surfaceLight, ans.b * surfaceLight, ans.a); 299 | } 300 | 301 | var newTexture = new Texture2D(WebMapConfig.TEXTURE_SIZE, WebMapConfig.TEXTURE_SIZE, TextureFormat.RGBA32, false); 302 | newTexture.SetPixels(newColors); 303 | byte[] pngBytes = newTexture.EncodeToPNG(); 304 | 305 | mapDataServer.mapImageData = pngBytes; 306 | try { 307 | File.WriteAllBytes(Path.Combine(worldDataPath, "map"), pngBytes); 308 | } catch { 309 | Debug.Log("WebMap: FAILED TO WRITE MAP FILE!"); 310 | } 311 | Debug.Log("WebMap: BUILDING MAP DONE!"); 312 | } 313 | } 314 | 315 | [HarmonyPatch(typeof (ZNet), "Start")] 316 | private class ZNetPatch { 317 | static void Postfix(List ___m_peers) { 318 | mapDataServer.players = ___m_peers; 319 | } 320 | } 321 | 322 | private static readonly int sayMethodHash = "Say".GetStableHashCode(); 323 | private static readonly int chatMessageMethodHash = "ChatMessage".GetStableHashCode(); 324 | 325 | [HarmonyPatch(typeof (ZRoutedRpc), "HandleRoutedRPC")] 326 | private class ZRoutedRpcPatch { 327 | static void Prefix(RoutedRPCData data) { 328 | ZNetPeer peer = ZNet.instance.GetPeer(data.m_senderPeerID); 329 | var steamid = ""; 330 | try { 331 | steamid = peer.m_rpc.GetSocket().GetHostName(); 332 | } catch {} 333 | 334 | if (data?.m_methodHash == sayMethodHash) { 335 | try { 336 | var zdoData = ZDOMan.instance.GetZDO(peer.m_characterID); 337 | var pos = zdoData.GetPosition(); 338 | ZPackage package = new ZPackage(data.m_parameters.GetArray()); 339 | int messageType = package.ReadInt(); 340 | string userName = package.ReadString(); 341 | string message = package.ReadString(); 342 | message = (message == null ? "" : message).Trim(); 343 | 344 | if (message.StartsWith("/pin")) { 345 | var messageParts = message.Split(' '); 346 | var pinType = "dot"; 347 | var startIdx = 1; 348 | if (messageParts.Length > 1 && ALLOWED_PINS.Contains(messageParts[1])) { 349 | pinType = messageParts[1]; 350 | startIdx = 2; 351 | } 352 | var pinText = ""; 353 | if (startIdx < messageParts.Length) { 354 | pinText = String.Join(" ", messageParts, startIdx, messageParts.Length - startIdx); 355 | } 356 | if (pinText.Length > 20) { 357 | pinText = pinText.Substring(0, 20); 358 | } 359 | var safePinsText = Regex.Replace(pinText, @"[^a-zA-Z0-9 ]", ""); 360 | 361 | var timestamp = DateTime.Now - unixEpoch; 362 | var pinId = $"{timestamp.TotalMilliseconds}-{UnityEngine.Random.Range(1000, 9999)}"; 363 | mapDataServer.AddPin(steamid, pinId, pinType, userName, pos, safePinsText); 364 | 365 | var usersPins = mapDataServer.pins.FindAll(pin => pin.StartsWith(steamid)); 366 | var numOverflowPins = usersPins.Count - WebMapConfig.MAX_PINS_PER_USER; 367 | for (var t = numOverflowPins; t > 0; t--) { 368 | var pinIdx = mapDataServer.pins.FindIndex(pin => pin.StartsWith(steamid)); 369 | mapDataServer.RemovePin(pinIdx); 370 | } 371 | SavePins(); 372 | } else if (message.StartsWith("/undoPin")) { 373 | var pinIdx = mapDataServer.pins.FindLastIndex(pin => pin.StartsWith(steamid)); 374 | if (pinIdx > -1) { 375 | mapDataServer.RemovePin(pinIdx); 376 | SavePins(); 377 | } 378 | } else if (message.StartsWith("/deletePin")) { 379 | var messageParts = message.Split(' '); 380 | var pinText = ""; 381 | if (messageParts.Length > 1) { 382 | pinText = String.Join(" ", messageParts, 1, messageParts.Length - 1); 383 | } 384 | 385 | var pinIdx = mapDataServer.pins.FindLastIndex(pin => { 386 | var pinParts = pin.Split(','); 387 | return pinParts[0] == steamid && pinParts[pinParts.Length - 1] == pinText; 388 | }); 389 | 390 | if (pinIdx > -1) { 391 | mapDataServer.RemovePin(pinIdx); 392 | SavePins(); 393 | } 394 | } 395 | //Debug.Log("SAY!!! " + messageType + " | " + userName + " | " + message); 396 | } catch {} 397 | } else if (data?.m_methodHash == chatMessageMethodHash) { 398 | try { 399 | ZPackage package = new ZPackage(data.m_parameters.GetArray()); 400 | Vector3 pos = package.ReadVector3(); 401 | int messageType = package.ReadInt(); 402 | string userName = package.ReadString(); 403 | // string message = package.ReadString(); 404 | // message = (message == null ? "" : message).Trim(); 405 | 406 | if (messageType == (int)Talker.Type.Ping) { 407 | mapDataServer.BroadcastPing(data.m_senderPeerID, userName, pos); 408 | } 409 | // Debug.Log("CHAT!!! " + pos + " | " + messageType + " | " + userName + " | " + message); 410 | } catch {} 411 | } 412 | } 413 | } 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /WebMap/WebMap.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | v4.8 6 | win-x64 7 | 8 | 9 | x64 10 | true 11 | portable 12 | false 13 | bin\Debug\ 14 | DEBUG;TRACE 15 | prompt 16 | 4 17 | 18 | 19 | x64 20 | portable 21 | true 22 | bin\Release\ 23 | TRACE 24 | prompt 25 | 4 26 | 27 | 28 | 29 | 30 | 31 | ..\libs\0Harmony.dll 32 | 33 | 34 | ..\libs\assembly_valheim.dll 35 | 36 | 37 | ..\libs\assembly_utils.dll 38 | 39 | 40 | ..\libs\BepInEx.dll 41 | 42 | 43 | ..\libs\BepInEx.Harmony.dll 44 | 45 | 46 | ..\libs\UnityEngine.dll 47 | 48 | 49 | ..\libs\UnityEngine.CoreModule.dll 50 | 51 | 52 | ..\libs\UnityEngine.ImageConversionModule.dll 53 | 54 | 55 | ..\libs\UnityEngine.UI.dll 56 | 57 | 58 | ..\libs\websocket-sharp.dll 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /WebMap/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "// lines that look like this are comments and dont do anything.": 0, 3 | 4 | "// http server listens on below port. Default: 3000": 0, 5 | "server_port": 3000, 6 | 7 | "// A larger explore_radius reveals the map more quickly. Default: 100": 0, 8 | "explore_radius": 100, 9 | 10 | "// How zoomed in should the web map start at? Higher is more zoomed in. Default: 200": 0, 11 | "default_zoom": 200, 12 | 13 | "// How many pins each client is allowed to make before old ones start being deleted. Default: 50": 0, 14 | "max_pins_per_user": 50, 15 | 16 | "// How often do we send position data to web browsers in seconds. Default: 0.5": 0, 17 | "player_update_interval": 0.5, 18 | 19 | "// How often do we update the fog texture on the server in seconds. Default: 1": 0, 20 | "update_fog_texture_interval": 1, 21 | 22 | "// How often do we save the fog texture in seconds. Default: 30": 0, 23 | "save_fog_texture_interval": 30, 24 | 25 | "// Should the server cache web files to be more performant? Default: true": 0, 26 | "cache_server_files": true, 27 | 28 | "// How large is the map texture? Probably dont change this. Default: 2048": 0, 29 | "texture_size": 2048, 30 | 31 | "// How many in game units does a map pixel represent? Probably dont change this. Default: 12": 0, 32 | "pixel_size": 12 33 | } 34 | -------------------------------------------------------------------------------- /WebMap/web-src/constants.js: -------------------------------------------------------------------------------- 1 | const constants = { 2 | CANVAS_WIDTH: 2048, 3 | CANVAS_HEIGHT: 2048, 4 | PIXEL_SIZE: 12, 5 | EXPLORE_RADIUS: 100, 6 | DEFAULT_ZOOM: 200 7 | }; 8 | 9 | constants.COORD_OFFSET = constants.CANVAS_WIDTH / 2; 10 | 11 | export default constants; 12 | -------------------------------------------------------------------------------- /WebMap/web-src/index.js: -------------------------------------------------------------------------------- 1 | import constants from "./constants"; 2 | import websocket from "./websocket"; 3 | import map from "./map"; 4 | import players from "./players"; 5 | import ui from "./ui"; 6 | 7 | const mapImage = document.createElement('img'); 8 | const fogImage = document.createElement('img'); 9 | 10 | const fetchMap = () => new Promise((res) => { 11 | fetch('map').then(res => res.blob()).then((mapBlob) => { 12 | mapImage.onload = res; 13 | mapImage.src = URL.createObjectURL(mapBlob); 14 | }); 15 | }); 16 | 17 | const fetchFog = () => new Promise((res) => { 18 | fogImage.onload = res; 19 | fogImage.src = 'fog'; 20 | }); 21 | 22 | const createStyleSheet = (styles = '') => { 23 | const style = document.createElement("style"); 24 | style.appendChild(document.createTextNode(styles)); 25 | document.head.appendChild(style); 26 | return style.sheet; 27 | }; 28 | 29 | const parseVector3 = str => { 30 | const strParts = str.split(','); 31 | return { 32 | x: parseFloat(strParts[0]), 33 | y: parseFloat(strParts[1]), 34 | z: parseFloat(strParts[2]), 35 | }; 36 | }; 37 | 38 | const fetchConfig = fetch('config').then(res => res.json()).then(config => { 39 | constants.CANVAS_WIDTH = config.texture_size || 2048; 40 | constants.CANVAS_HEIGHT = config.texture_size || 2048; 41 | constants.PIXEL_SIZE = config.pixel_size || 12; 42 | constants.EXPLORE_RADIUS = config.explore_radius || 100; 43 | constants.UPDATE_INTERVAL = config.update_interval || 0.5; 44 | constants.WORLD_NAME = config.world_name; 45 | constants.WORLD_START_POSITION = parseVector3(config.world_start_pos); 46 | constants.DEFAULT_ZOOM = config.default_zoom || 200; 47 | document.title = `Valheim WebMap - ${constants.WORLD_NAME}`; 48 | createStyleSheet(` 49 | .mapIcon.player { 50 | transition: top ${constants.UPDATE_INTERVAL}s linear, left ${constants.UPDATE_INTERVAL}s linear; 51 | } 52 | .map.smooth { 53 | transition: top ${constants.UPDATE_INTERVAL}s linear, left ${constants.UPDATE_INTERVAL}s linear; 54 | } 55 | `); 56 | }); 57 | 58 | const setup = async () => { 59 | websocket.init(); 60 | players.init(); 61 | 62 | await Promise.all([ 63 | fetchMap(), 64 | fetchFog(), 65 | fetchConfig 66 | ]); 67 | 68 | map.init({ 69 | mapImage, 70 | fogImage, 71 | zoom: constants.DEFAULT_ZOOM 72 | }); 73 | 74 | map.addIcon({ 75 | type: 'start', 76 | x: constants.WORLD_START_POSITION.x, 77 | z: constants.WORLD_START_POSITION.z 78 | }); 79 | 80 | const pings = {}; 81 | websocket.addActionListener('ping', (ping) => { 82 | let mapIcon = pings[ping.playerId]; 83 | if (!mapIcon) { 84 | mapIcon = { ...ping }; 85 | mapIcon.type = 'ping'; 86 | mapIcon.text = ping.name; 87 | map.addIcon(mapIcon, false); 88 | pings[ping.playerId] = mapIcon; 89 | } 90 | mapIcon.x = ping.x; 91 | mapIcon.z = ping.z; 92 | map.updateIcons(); 93 | 94 | clearTimeout(mapIcon.timeoutId); 95 | mapIcon.timeoutId = setTimeout(() => { 96 | delete pings[ping.playerId]; 97 | map.removeIcon(mapIcon); 98 | }, 8000); 99 | }); 100 | 101 | fetch('pins').then(res => res.text()).then(text => { 102 | const lines = text.split('\n'); 103 | lines.forEach(line => { 104 | const lineParts = line.split(','); 105 | if (lineParts.length > 5) { 106 | const pin = { 107 | id: lineParts[1], 108 | uid: lineParts[0], 109 | type: lineParts[2], 110 | name: lineParts[3], 111 | x: lineParts[4], 112 | z: lineParts[5], 113 | text: lineParts[6], 114 | static: true 115 | }; 116 | map.addIcon(pin, false); 117 | } 118 | }); 119 | map.updateIcons(); 120 | }); 121 | 122 | websocket.addActionListener('pin', (pin) => { 123 | map.addIcon(pin); 124 | }); 125 | 126 | websocket.addActionListener('rmpin', (pinid) => { 127 | map.removeIconById(pinid); 128 | }); 129 | 130 | window.addEventListener('resize', () => { 131 | map.update(); 132 | }); 133 | 134 | ui.menuBtn.addEventListener('click', () => { 135 | ui.menu.classList.toggle('menuOpen'); 136 | }); 137 | 138 | const closeMenu = (e) => { 139 | const inMenu = e.target.closest('.menu'); 140 | const inMenuBtn = e.target.closest('.menu-btn'); 141 | if (!inMenu && !inMenuBtn) { 142 | ui.menu.classList.remove('menuOpen'); 143 | } 144 | }; 145 | window.addEventListener('mousedown', closeMenu); 146 | window.addEventListener('touchstart', closeMenu); 147 | 148 | const hideCheckboxes = ui.menu.querySelectorAll('.hideIconCheckbox'); 149 | hideCheckboxes.forEach(el => { 150 | el.addEventListener('change', () => { 151 | map.setIconHidden(el.dataset.hide, el.checked || ui.hideAll.checked); 152 | if (el.dataset.hide === 'all') { 153 | hideCheckboxes.forEach(el2 => { 154 | map.setIconHidden(el2.dataset.hide, el.checked || el2.checked); 155 | }); 156 | } 157 | map.updateIcons(); 158 | }); 159 | }); 160 | 161 | ui.hidePlayerList.addEventListener('change', () => { 162 | if (ui.hidePlayerList.checked) { 163 | ui.playerListContainer.style.right = -ui.playerListContainer.offsetWidth + 'px'; 164 | } else { 165 | ui.playerListContainer.style.right = 0; 166 | } 167 | }); 168 | }; 169 | 170 | setup(); 171 | -------------------------------------------------------------------------------- /WebMap/web-src/map.js: -------------------------------------------------------------------------------- 1 | import ui from './ui'; 2 | import constants from "./constants"; 3 | import onPointers from "./onPointers"; 4 | 5 | const { canvas, map, mapBorder, mapBorderCircle } = ui; 6 | 7 | let width = constants.CANVAS_WIDTH; 8 | let height = constants.CANVAS_HEIGHT; 9 | let exploreRadius = constants.EXPLORE_RADIUS; 10 | let pixelSize = constants.PIXEL_SIZE; 11 | let coordOffset = constants.COORD_OFFSET; 12 | 13 | // preload map icons. 14 | const mapIconImage = document.createElement('img'); 15 | mapIconImage.src = 'mapIcons.png'; 16 | 17 | const ctx = canvas.getContext('2d'); 18 | 19 | let mapImage; 20 | let fogImage; 21 | const fogCanvas = document.createElement('canvas'); 22 | const fogCanvasCtx = fogCanvas.getContext('2d'); 23 | 24 | let currentZoom = 100; 25 | 26 | const mapIcons = []; 27 | const hiddenIcons = {}; 28 | let followIcon; 29 | 30 | const createIconEl = (iconObj) => { 31 | const iconEl = document.createElement('div'); 32 | iconEl.id = iconObj.id; 33 | iconEl.className = `mapText mapIcon ${iconObj.type}`; 34 | if (iconObj.zIndex) { 35 | iconEl.style.zIndex = iconObj.zIndex; 36 | } 37 | const iconTextEl = document.createElement('div'); 38 | iconTextEl.className = 'center text'; 39 | iconTextEl.textContent = iconObj.text; 40 | iconEl.appendChild(iconTextEl); 41 | if (iconObj.node) { 42 | iconEl.appendChild(iconObj.node); 43 | } 44 | return iconEl; 45 | }; 46 | 47 | const centerOnIcon = (iconObj) => { 48 | const rect = iconObj.el.getBoundingClientRect(); 49 | const deltaX = window.innerWidth / 2 - rect.left; 50 | const deltaY = window.innerHeight / 2 - rect.top; 51 | if (Math.abs(deltaX) > 0.5 || Math.abs(deltaY) > 0.5) { 52 | map.style.left = deltaX + map.offsetLeft + 'px'; 53 | map.style.top = deltaY + map.offsetTop + 'px'; 54 | } 55 | }; 56 | 57 | const setFollowIcon = (iconObj) => { 58 | followIcon = iconObj; 59 | if (followIcon) { 60 | centerOnIcon(followIcon); 61 | } 62 | }; 63 | 64 | const updateIcons = () => { 65 | mapIcons.forEach(iconObj => { 66 | let firstRender = false; 67 | if (!iconObj.el) { 68 | firstRender = true; 69 | iconObj.el = createIconEl(iconObj); 70 | map.appendChild(iconObj.el); 71 | } 72 | const isIconHidden = hiddenIcons[iconObj.type]; 73 | iconObj.el.style.display = isIconHidden ? 'none' : 'block'; 74 | if (!firstRender && iconObj.static) { 75 | return; 76 | } 77 | 78 | const imgX = iconObj.x / pixelSize + coordOffset; 79 | const imgY = height - (iconObj.z / pixelSize + coordOffset); 80 | 81 | iconObj.el.style.left = 100 * imgX / width + '%'; 82 | iconObj.el.style.top = 100 * imgY / height + '%'; 83 | }); 84 | 85 | if (followIcon) { 86 | centerOnIcon(followIcon); 87 | } 88 | }; 89 | 90 | window.addEventListener('mousemove', e => { 91 | const canvasOffsetScale = map.offsetWidth / width; 92 | const x = pixelSize * (-coordOffset + (e.clientX - map.offsetLeft) / canvasOffsetScale); 93 | const y = pixelSize * (height - coordOffset + (map.offsetTop - e.clientY) / canvasOffsetScale); 94 | ui.coords.textContent = `${x.toFixed(2)} , ${y.toFixed(2)}`; 95 | }); 96 | 97 | const addIcon = (iconObj, update = true) => { 98 | if (!iconObj.id) { 99 | iconObj.id = `id_${Date.now()}_${Math.random()}`; 100 | } 101 | mapIcons.push(iconObj); 102 | if (update) { 103 | updateIcons(); 104 | } 105 | }; 106 | 107 | const removeIcon = (iconObj) => { 108 | const idx = mapIcons.indexOf(iconObj); 109 | if (idx > -1) { 110 | mapIcons.splice(idx, 1); 111 | if (iconObj.el) { 112 | iconObj.el.remove(); 113 | iconObj.el = undefined; 114 | } 115 | } 116 | }; 117 | 118 | const removeIconById = (iconId) => { 119 | const iconToRemove = mapIcons.find(icon => icon.id === iconId); 120 | if (iconToRemove) { 121 | removeIcon(iconToRemove); 122 | } 123 | }; 124 | 125 | const setIconHidden = (type, isHidden) => { 126 | hiddenIcons[type] = isHidden; 127 | }; 128 | 129 | const redrawMap = () => { 130 | ctx.clearRect(0, 0, width, height); 131 | ctx.globalCompositeOperation = 'source-over'; 132 | ctx.drawImage(mapImage, 0, 0); 133 | ctx.globalCompositeOperation = 'multiply'; 134 | ctx.drawImage(fogCanvas, 0, 0); 135 | 136 | updateIcons(); 137 | }; 138 | 139 | const explore = (mapX, mapZ) => { 140 | const radius = exploreRadius / pixelSize; 141 | const x = mapX / pixelSize + coordOffset; 142 | const y = height - (mapZ / pixelSize + coordOffset); 143 | fogCanvasCtx.beginPath(); 144 | fogCanvasCtx.arc(x, y, radius, 0, 2 * Math.PI); 145 | fogCanvasCtx.fill(); 146 | redrawMap(); 147 | }; 148 | 149 | const setZoom = function(zoomP, zoomTowardsX, zoomTowardsY) { 150 | if (zoomTowardsX === undefined) { 151 | zoomTowardsX = window.innerWidth / 2; 152 | zoomTowardsY = window.innerHeight / 2; 153 | } 154 | const oldZoom = currentZoom; 155 | const minZoom = 50; 156 | const maxZoom = 8000 * devicePixelRatio; 157 | zoomP = Math.min(Math.max(Math.round(zoomP), minZoom), maxZoom); 158 | currentZoom = zoomP; 159 | map.style.width = `${zoomP}%`; 160 | map.style.height = map.offsetWidth + 'px'; 161 | 162 | const zoomRatio = currentZoom / oldZoom; 163 | map.style.left = zoomRatio * (map.offsetLeft - zoomTowardsX) + zoomTowardsX + 'px'; 164 | map.style.top = zoomRatio * (map.offsetTop - zoomTowardsY) + zoomTowardsY + 'px'; 165 | 166 | updateIcons(); 167 | }; 168 | 169 | let zoomingClassTimeout; 170 | const removeZoomingClass = () => { 171 | clearTimeout(zoomingClassTimeout); 172 | zoomingClassTimeout = setTimeout(() => { 173 | map.classList.remove('zooming'); 174 | }, 100); 175 | }; 176 | 177 | const init = (options) => { 178 | width = constants.CANVAS_WIDTH; 179 | height = constants.CANVAS_HEIGHT; 180 | exploreRadius = constants.EXPLORE_RADIUS; 181 | pixelSize = constants.PIXEL_SIZE; 182 | coordOffset = constants.COORD_OFFSET; 183 | 184 | canvas.width = width; 185 | canvas.height = height; 186 | map.style.width = '100%'; 187 | map.style.height = map.offsetWidth + 'px'; 188 | map.style.left = (window.innerWidth - map.offsetWidth) / 2 + 'px'; 189 | map.style.top = (window.innerHeight - map.offsetHeight) / 2 + 'px'; 190 | fogCanvas.width = width; 191 | fogCanvas.height = height; 192 | fogCanvasCtx.fillStyle = '#ffffff'; 193 | 194 | mapBorder.setAttribute("viewBox", `0 0 ${width} ${height}`); 195 | mapBorderCircle.setAttribute("cx", width / 2); 196 | mapBorderCircle.setAttribute("cy", width / 2); 197 | mapBorderCircle.setAttribute("r", width * 0.4275); 198 | 199 | mapImage = options.mapImage; 200 | fogImage = options.fogImage; 201 | 202 | fogCanvasCtx.drawImage(fogImage, 0, 0); 203 | 204 | redrawMap(); 205 | 206 | const zoomChange = (e, mult = 1) => { 207 | map.classList.add('zooming'); 208 | const oldZoom = currentZoom; 209 | const zoomAmount = Math.max(Math.floor(oldZoom / 5), 1) * mult; 210 | const scrollAmt = e.deltaY === 0 ? e.deltaX : e.deltaY; 211 | if (scrollAmt > 0) { 212 | // zoom out. 213 | setZoom(oldZoom - zoomAmount, e.clientX, e.clientY); 214 | } else { 215 | // zoom in. 216 | setZoom(oldZoom + zoomAmount, e.clientX, e.clientY); 217 | } 218 | removeZoomingClass(); 219 | }; 220 | 221 | if (options.zoom) { 222 | setZoom(options.zoom); 223 | } 224 | 225 | window.addEventListener('wheel', zoomChange); 226 | window.addEventListener('resize', () => { 227 | map.style.height = map.offsetWidth + 'px'; 228 | }); 229 | 230 | const canvasPreDragPos = {}; 231 | let isZooming = false; 232 | let lastZoomDist; 233 | onPointers(window, { 234 | down: (pointers) => { 235 | if (pointers.length === 1) { 236 | canvasPreDragPos.x = map.offsetLeft; 237 | canvasPreDragPos.y = map.offsetTop; 238 | } else if (pointers.length === 2) { 239 | isZooming = true; 240 | lastZoomDist = undefined; 241 | } 242 | }, 243 | move: (pointers) => { 244 | if (pointers.length === 1 && !isZooming && !followIcon) { 245 | const e = pointers[0].event; 246 | map.style.left = canvasPreDragPos.x + (e.clientX - pointers[0].downEvent.clientX) + 'px'; 247 | map.style.top = canvasPreDragPos.y + (e.clientY - pointers[0].downEvent.clientY) + 'px'; 248 | 249 | updateIcons(); 250 | } else if (pointers.length === 2) { 251 | const x1 = pointers[0].event.clientX; 252 | const y1 = pointers[0].event.clientY; 253 | const x2 = pointers[1].event.clientX; 254 | const y2 = pointers[1].event.clientY; 255 | const diffX = x1 - x2; 256 | const diffY = y1 - y2; 257 | const dist = Math.sqrt(diffX * diffX + diffY * diffY); 258 | if (lastZoomDist) { 259 | const diffDist = (lastZoomDist - dist) || -1; 260 | zoomChange({ 261 | deltaY: diffDist, 262 | clientX: (x1 + x2) / 2, 263 | clientY: (y1 + y2) / 2 264 | }, 0.08); 265 | } 266 | lastZoomDist = dist; 267 | } 268 | }, 269 | up: (pointers) => { 270 | if (pointers.length === 0) { 271 | isZooming = false; 272 | } 273 | } 274 | }); 275 | }; 276 | 277 | export default { 278 | init, 279 | addIcon, 280 | removeIcon, 281 | removeIconById, 282 | setIconHidden, 283 | explore, 284 | centerOnIcon, 285 | setFollowIcon, 286 | update: redrawMap, 287 | updateIcons, 288 | canvas 289 | }; 290 | 291 | -------------------------------------------------------------------------------- /WebMap/web-src/onPointers.js: -------------------------------------------------------------------------------- 1 | const onPointers = (element, options) => { 2 | const downPointers = new Map(); 3 | let downPointerArr = []; 4 | 5 | const down = e => { 6 | const newPointer = { downEvent: e, event: e }; 7 | downPointers.set(e.pointerId, newPointer); 8 | downPointerArr.push(newPointer); 9 | if (options.down) { 10 | options.down(downPointerArr); 11 | } 12 | }; 13 | 14 | const move = e => { 15 | const currentPointer = downPointers.get(e.pointerId); 16 | if (currentPointer) { 17 | currentPointer.event = e; 18 | if (options.move) { 19 | options.move(downPointerArr); 20 | } 21 | } 22 | }; 23 | 24 | const up = e => { 25 | downPointers.delete(e.pointerId); 26 | downPointerArr = downPointerArr.filter(pointer => pointer.event.pointerId !== e.pointerId); 27 | if (options.up) { 28 | options.up(downPointerArr); 29 | } 30 | }; 31 | 32 | element.addEventListener('pointerdown', down); 33 | window.addEventListener('pointermove', move); 34 | window.addEventListener('pointerup', up); 35 | window.addEventListener('pointercancel', up); 36 | }; 37 | 38 | export default onPointers; 39 | -------------------------------------------------------------------------------- /WebMap/web-src/players.js: -------------------------------------------------------------------------------- 1 | import ui, { createUi } from "./ui"; 2 | import websocket from "./websocket"; 3 | import map from "./map"; 4 | 5 | const playerMapIcons = {}; 6 | let followingPlayer; 7 | 8 | const followPlayer = (playerMapIcon) => { 9 | if (followingPlayer) { 10 | followingPlayer.playerListEntry.el.classList.remove('selected'); 11 | } 12 | if (playerMapIcon && playerMapIcon !== followingPlayer) { 13 | followingPlayer = playerMapIcon; 14 | followingPlayer.playerListEntry.el.classList.add('selected'); 15 | ui.map.classList.remove('smooth'); 16 | map.setFollowIcon(playerMapIcon); 17 | ui.topMessage.textContent = `Following ${followingPlayer.name}`; 18 | setTimeout(() => { 19 | ui.map.classList.add('smooth'); 20 | }, 0); 21 | } else { 22 | followingPlayer = null; 23 | map.setFollowIcon(null); 24 | ui.map.classList.remove('smooth'); 25 | ui.topMessage.textContent = ''; 26 | } 27 | }; 28 | 29 | const init = () => { 30 | websocket.addActionListener('players', (players) => { 31 | players.forEach((player) => { 32 | let playerMapIcon = playerMapIcons[player.id]; 33 | if (!playerMapIcon) { 34 | // new player 35 | const playerListEntry = createUi(` 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | `); 44 | playerListEntry.ui.name.textContent = player.name; 45 | ui.playerList.appendChild(playerListEntry.el); 46 | playerMapIcon = { 47 | ...player, 48 | type: 'player', 49 | text: player.name, 50 | zIndex: 5, 51 | playerListEntry 52 | }; 53 | if (!player.hidden) { 54 | map.addIcon(playerMapIcon, false); 55 | } else { 56 | playerListEntry.ui.hpBar.style.display = 'none'; 57 | } 58 | playerMapIcons[player.id] = playerMapIcon; 59 | playerListEntry.el.addEventListener('click', () => { 60 | if (!playerMapIcon.hidden) { 61 | if (ui.playerListTut) { 62 | ui.playerListTut.remove(); 63 | ui.playerListTut = undefined; 64 | } 65 | followPlayer(playerMapIcon); 66 | } 67 | }); 68 | } 69 | 70 | if (!player.hidden && playerMapIcon.hidden) { 71 | // no longer hidden 72 | playerMapIcon.hidden = player.hidden; 73 | map.addIcon(playerMapIcon, false); 74 | playerMapIcon.playerListEntry.ui.hpBar.style.display = 'block'; 75 | } else if (player.hidden && !playerMapIcon.hidden) { 76 | // becomming hidden 77 | playerMapIcon.hidden = player.hidden; 78 | map.removeIcon(playerMapIcon); 79 | playerMapIcon.playerListEntry.ui.hpBar.style.display = 'none'; 80 | if (followingPlayer === playerMapIcon) { 81 | followPlayer(null); 82 | } 83 | } 84 | 85 | playerMapIcon.lastUpdate = Date.now(); 86 | playerMapIcon.x = player.x; 87 | playerMapIcon.z = player.z; 88 | 89 | if (!player.hidden) { 90 | playerMapIcon.playerListEntry.ui.hp.style.width = `${ 91 | 100 * Math.max(player.health / player.maxHealth, 0) 92 | }%`; 93 | playerMapIcon.playerListEntry.ui.hpText.textContent = `${ 94 | Math.round(Math.max(player.health, 0)) 95 | } / ${ 96 | Math.round(player.maxHealth) 97 | }`; 98 | 99 | map.explore(player.x, player.z); 100 | } 101 | }); 102 | map.updateIcons(); 103 | }); 104 | 105 | setInterval(() => { 106 | // clean up disconnected players. 107 | const now = Date.now(); 108 | Object.keys(playerMapIcons).forEach((key) => { 109 | const playerMapIcon = playerMapIcons[key]; 110 | if (now - playerMapIcon.lastUpdate > 5000) { 111 | map.removeIcon(playerMapIcon); 112 | if (playerMapIcon === followingPlayer) { 113 | followPlayer(null); 114 | } 115 | playerMapIcon.playerListEntry.el.remove(); 116 | delete playerMapIcons[key]; 117 | } 118 | }); 119 | }, 2000); 120 | }; 121 | 122 | export default { 123 | init 124 | }; 125 | -------------------------------------------------------------------------------- /WebMap/web-src/ui.js: -------------------------------------------------------------------------------- 1 | const ui = {}; 2 | const allUi = document.querySelectorAll('[data-id]'); 3 | allUi.forEach(el => { 4 | ui[el.dataset.id] = el; 5 | }); 6 | 7 | const tempDiv = document.createElement('div'); 8 | export const createUi = (html) => { 9 | tempDiv.innerHTML = html; 10 | 11 | const uiEls = {}; 12 | const dataEls = tempDiv.querySelectorAll('[data-id]'); 13 | dataEls.forEach(el => { 14 | uiEls[el.dataset.id] = el; 15 | }); 16 | 17 | return { 18 | el: tempDiv.children[0], 19 | ui: uiEls 20 | }; 21 | }; 22 | 23 | export default ui; 24 | -------------------------------------------------------------------------------- /WebMap/web-src/websocket.js: -------------------------------------------------------------------------------- 1 | const actionListeners = {}; 2 | 3 | const addActionListener = (type, func) => { 4 | const listeners = actionListeners[type] || []; 5 | listeners.push(func); 6 | actionListeners[type] = listeners; 7 | }; 8 | 9 | const actions = { 10 | players: (lines, message) => { 11 | const msg = message.replace(/^players\n/, ''); 12 | const playerSections = msg.split('\n\n'); 13 | const playerData = []; 14 | playerSections.forEach(playerSection => { 15 | const playerLines = playerSection.split('\n'); 16 | const newPlayer = { 17 | id: playerLines[0], 18 | name: playerLines[1], 19 | health: parseFloat(playerLines[3]), 20 | maxHealth: parseFloat(playerLines[4]) 21 | }; 22 | 23 | if (playerLines[2] !== 'hidden') { 24 | const xyz = playerLines[2].split(',').map(parseFloat); 25 | newPlayer.x = xyz[0]; 26 | newPlayer.y = xyz[1]; 27 | newPlayer.z = xyz[2]; 28 | } else { 29 | newPlayer.hidden = true; 30 | } 31 | playerData.push(newPlayer); 32 | }); 33 | // const fakePlayer = playerData[0]; 34 | // if (fakePlayer) { 35 | // playerData.push({ ...fakePlayer, x: fakePlayer.x + 1000, z: fakePlayer.z - 2000, id: 'asd', name: 'lolol' }); 36 | // } 37 | 38 | actionListeners.players.forEach(func => { 39 | func(playerData); 40 | }); 41 | }, 42 | ping: (lines) => { 43 | const xz = lines[2].split(','); 44 | const ping = { 45 | playerId: lines[0], 46 | name: lines[1], 47 | x: parseFloat(xz[0]), 48 | z: parseFloat(xz[1]) 49 | }; 50 | actionListeners.ping.forEach(func => { 51 | func(ping); 52 | }); 53 | }, 54 | pin: (lines) => { 55 | const xz = lines[4].split(',').map(parseFloat); 56 | const pin = { 57 | id: lines[1], 58 | uid: lines[0], 59 | type: lines[2], 60 | name: lines[3], 61 | x: xz[0], 62 | z: xz[1], 63 | text: lines[5] 64 | }; 65 | actionListeners.pin.forEach(func => { 66 | func(pin); 67 | }); 68 | }, 69 | rmpin: (lines) => { 70 | actionListeners.rmpin.forEach(func => { 71 | func(lines[0]); 72 | }); 73 | } 74 | }; 75 | 76 | Object.keys(actions).forEach(key => { 77 | actionListeners[key] = []; 78 | }); 79 | 80 | let connectionTries = 0; 81 | const init = () => { 82 | const websocketUrl = location.href.split('?')[0].replace(/^http/, 'ws'); 83 | const ws = new WebSocket(websocketUrl); 84 | ws.addEventListener('message', (e) => { 85 | const message = e.data.trim(); 86 | const lines = message.split('\n'); 87 | const action = lines.shift(); 88 | const actionFunc = actions[action]; 89 | if (actionFunc) { 90 | actionFunc(lines, message); 91 | } else { 92 | console.log("unknown websocket message: ", e.data); 93 | } 94 | }); 95 | 96 | ws.addEventListener('open', () => { 97 | connectionTries = 0; 98 | }); 99 | 100 | ws.addEventListener('close', () => { 101 | connectionTries++; 102 | const seconds = Math.min(connectionTries * (connectionTries + 1), 120); 103 | setTimeout(init, seconds * 1000); 104 | }); 105 | }; 106 | 107 | export default { 108 | init, 109 | addActionListener 110 | }; 111 | 112 | -------------------------------------------------------------------------------- /WebMap/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Valheim WebMap 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 16 | 17 |
18 |
19 | 24 | 59 |
60 |
Players
61 |
62 |
Click player
to toggle follow.
63 |
64 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /WebMap/web/mapIcons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylepaulsen/ValheimWebMap/df2920106aac50c226e310e7ba528ce723be4fb1/WebMap/web/mapIcons.png -------------------------------------------------------------------------------- /WebMap/web/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin: 0; 7 | padding: 0; 8 | background: #000; 9 | overflow: hidden; 10 | height: 100%; 11 | font-family: sans-serif; 12 | touch-action: none; 13 | } 14 | 15 | canvas { 16 | image-rendering: crisp-edges; 17 | image-rendering: pixelated; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | @keyframes pulse { 23 | from { transform: translate(-50%, -50%) scale(1); } 24 | 50% { transform: translate(-50%, -50%) scale(1.5); } 25 | to { transform: translate(-50%, -50%) scale(1); } 26 | } 27 | 28 | 29 | .map { 30 | position: fixed; 31 | } 32 | 33 | .mapBorder { 34 | width: 100%; 35 | height: 100%; 36 | pointer-events: none; 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | } 41 | 42 | body .map.zooming { 43 | transition: none; 44 | } 45 | 46 | .menu-btn { 47 | position: fixed; 48 | top: 8px; 49 | left: 8px; 50 | padding: 8px; 51 | cursor: pointer; 52 | -webkit-tap-highlight-color: transparent; 53 | z-index: 20; 54 | } 55 | 56 | .menu-btn:hover .bar { 57 | background: #ccc; 58 | } 59 | 60 | .menu-btn .bar { 61 | background: #fff; 62 | width: 30px; 63 | height: 3px; 64 | margin-bottom: 4px; 65 | border-radius: 10px 0; 66 | box-shadow: 0px 0px 3px 1.5px #000; 67 | transition: background 0.1s ease-in-out; 68 | } 69 | 70 | .menu-btn .bar:last-child { 71 | margin-bottom: 0; 72 | } 73 | 74 | .menu { 75 | background: rgba(0, 0, 0, 0.7); 76 | color: #fff; 77 | padding: 10px 16px 10px 12px; 78 | position: fixed; 79 | top: 46px; 80 | /* left: 14px; */ 81 | left: -180px; 82 | font-size: 14px; 83 | transition: left 0.3s ease-in-out; 84 | z-index: 20; 85 | } 86 | 87 | .menu.menuOpen { 88 | left: 0px; 89 | } 90 | 91 | .menu label { 92 | cursor: pointer; 93 | user-select: none; 94 | } 95 | 96 | .menu .inputRow { 97 | display: flex; 98 | align-items: center; 99 | height: 26px; 100 | } 101 | 102 | .inputRow input { 103 | margin-right: 8px; 104 | } 105 | 106 | .mapText { 107 | color: #fff; 108 | font-size: 12px; 109 | white-space: nowrap; 110 | text-shadow: 0 0 6px #000, 0 0 6px #000, 0 0 3px #000; 111 | user-select: none; 112 | pointer-events: none; 113 | } 114 | 115 | .coords { 116 | position: fixed; 117 | right: 8px; 118 | bottom: 6px; 119 | z-index: 10; 120 | font-size: 10px; 121 | } 122 | 123 | .mapIcon { 124 | position: absolute; 125 | width: 32px; 126 | height: 32px; 127 | background-image: url('mapIcons.png'); 128 | background-size: 160px; 129 | transform: translate(-50%, -50%); 130 | } 131 | 132 | .mapIcon .center { 133 | position: absolute; 134 | left: 50%; 135 | transform: translate(-50%, 0); 136 | } 137 | 138 | .mapIcon .text { 139 | bottom: -11px; 140 | } 141 | 142 | .mapIcon.player { 143 | background-position: 0 0; 144 | } 145 | 146 | .mapIcon.fire { 147 | background-position: -32px 0; 148 | } 149 | 150 | .mapIcon.dot { 151 | background-position: -64px 0; 152 | } 153 | 154 | .mapIcon.mine { 155 | background-position: -96px 0; 156 | } 157 | 158 | .mapIcon.house { 159 | background-position: -128px 0; 160 | } 161 | 162 | .mapIcon.cave { 163 | background-position: 0 -32px; 164 | } 165 | 166 | .mapIcon.boss { 167 | background-position: -32px -32px; 168 | } 169 | 170 | .mapIcon.start { 171 | background-position: -64px -32px; 172 | } 173 | 174 | .mapIcon.ping { 175 | background-position: -96px -32px; 176 | animation: pulse 1.2s ease-in-out infinite; 177 | } 178 | 179 | .menuMapIcon { 180 | position: static; 181 | transform: scale(0.7); 182 | } 183 | 184 | .hpBar { 185 | border: 1px solid #f55; 186 | height: 9px; 187 | width: 100%; 188 | position: relative; 189 | } 190 | 191 | .hp { 192 | width: 100%; 193 | height: 100%; 194 | background: #f55; 195 | transition: width 0.5s linear; 196 | } 197 | 198 | .hpText { 199 | position: absolute; 200 | font-size: 10px; 201 | line-height: 8px; 202 | top: 0; 203 | left: 50%; 204 | transform: translate(-50%, 0); 205 | white-space: nowrap; 206 | } 207 | 208 | .playerListContainer { 209 | background: rgba(0, 0, 0, 0.7); 210 | color: #fff; 211 | padding: 10px 0; 212 | position: fixed; 213 | top: 0px; 214 | right: 0px; 215 | font-size: 12px; 216 | transition: right 0.3s ease-in-out; 217 | z-index: 20; 218 | min-width: 100px; 219 | text-align: center; 220 | } 221 | 222 | .playerListTitle { 223 | font-size: 16px; 224 | margin-bottom: 6px; 225 | } 226 | 227 | .playerListEntry { 228 | padding: 0 16px; 229 | cursor: pointer; 230 | padding: 3px 16px; 231 | } 232 | 233 | .playerListEntry.selected { 234 | background: rgba(135, 134, 202, 0.75); 235 | } 236 | 237 | .playerListTut { 238 | font-size: 12px; 239 | margin-top: 16px; 240 | } 241 | 242 | .topMessage { 243 | position: fixed; 244 | top: 6px; 245 | left: 50%; 246 | transform: translate(-50%, 0); 247 | color: #fff; 248 | text-shadow: 0 0 6px #000, 0 0 6px #000, 0 0 3px #000; 249 | font-size: 24px; 250 | text-align: center; 251 | } 252 | -------------------------------------------------------------------------------- /libs/0Harmony.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylepaulsen/ValheimWebMap/df2920106aac50c226e310e7ba528ce723be4fb1/libs/0Harmony.dll -------------------------------------------------------------------------------- /libs/BepInEx.Harmony.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylepaulsen/ValheimWebMap/df2920106aac50c226e310e7ba528ce723be4fb1/libs/BepInEx.Harmony.dll -------------------------------------------------------------------------------- /libs/BepInEx.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylepaulsen/ValheimWebMap/df2920106aac50c226e310e7ba528ce723be4fb1/libs/BepInEx.dll -------------------------------------------------------------------------------- /libs/websocket-sharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylepaulsen/ValheimWebMap/df2920106aac50c226e310e7ba528ce723be4fb1/libs/websocket-sharp.dll -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WebMap", 3 | "description": "A Valheim dedicated server mod to host a web accessible map.", 4 | "website_url": "https://github.com/kylepaulsen/ValheimWebMap", 5 | "version_number": "1.2.0", 6 | "dependencies": [] 7 | } 8 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmap", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@discoveryjs/json-ext": { 8 | "version": "0.5.2", 9 | "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz", 10 | "integrity": "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==", 11 | "dev": true 12 | }, 13 | "@types/eslint": { 14 | "version": "7.2.9", 15 | "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.9.tgz", 16 | "integrity": "sha512-SdAAXZNvWfhtf3X3y1cbbCZhP3xyPh7mfTvzV6CgfWc/ZhiHpyr9bVroe2/RCHIf7gczaNcprhaBLsx0CCJHQA==", 17 | "dev": true, 18 | "requires": { 19 | "@types/estree": "*", 20 | "@types/json-schema": "*" 21 | } 22 | }, 23 | "@types/eslint-scope": { 24 | "version": "3.7.0", 25 | "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", 26 | "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", 27 | "dev": true, 28 | "requires": { 29 | "@types/eslint": "*", 30 | "@types/estree": "*" 31 | } 32 | }, 33 | "@types/estree": { 34 | "version": "0.0.46", 35 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", 36 | "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==", 37 | "dev": true 38 | }, 39 | "@types/json-schema": { 40 | "version": "7.0.7", 41 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", 42 | "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", 43 | "dev": true 44 | }, 45 | "@types/node": { 46 | "version": "14.14.37", 47 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", 48 | "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", 49 | "dev": true 50 | }, 51 | "@webassemblyjs/ast": { 52 | "version": "1.11.0", 53 | "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", 54 | "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", 55 | "dev": true, 56 | "requires": { 57 | "@webassemblyjs/helper-numbers": "1.11.0", 58 | "@webassemblyjs/helper-wasm-bytecode": "1.11.0" 59 | } 60 | }, 61 | "@webassemblyjs/floating-point-hex-parser": { 62 | "version": "1.11.0", 63 | "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", 64 | "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==", 65 | "dev": true 66 | }, 67 | "@webassemblyjs/helper-api-error": { 68 | "version": "1.11.0", 69 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", 70 | "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==", 71 | "dev": true 72 | }, 73 | "@webassemblyjs/helper-buffer": { 74 | "version": "1.11.0", 75 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", 76 | "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==", 77 | "dev": true 78 | }, 79 | "@webassemblyjs/helper-numbers": { 80 | "version": "1.11.0", 81 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", 82 | "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", 83 | "dev": true, 84 | "requires": { 85 | "@webassemblyjs/floating-point-hex-parser": "1.11.0", 86 | "@webassemblyjs/helper-api-error": "1.11.0", 87 | "@xtuc/long": "4.2.2" 88 | } 89 | }, 90 | "@webassemblyjs/helper-wasm-bytecode": { 91 | "version": "1.11.0", 92 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", 93 | "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==", 94 | "dev": true 95 | }, 96 | "@webassemblyjs/helper-wasm-section": { 97 | "version": "1.11.0", 98 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", 99 | "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", 100 | "dev": true, 101 | "requires": { 102 | "@webassemblyjs/ast": "1.11.0", 103 | "@webassemblyjs/helper-buffer": "1.11.0", 104 | "@webassemblyjs/helper-wasm-bytecode": "1.11.0", 105 | "@webassemblyjs/wasm-gen": "1.11.0" 106 | } 107 | }, 108 | "@webassemblyjs/ieee754": { 109 | "version": "1.11.0", 110 | "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", 111 | "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", 112 | "dev": true, 113 | "requires": { 114 | "@xtuc/ieee754": "^1.2.0" 115 | } 116 | }, 117 | "@webassemblyjs/leb128": { 118 | "version": "1.11.0", 119 | "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", 120 | "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", 121 | "dev": true, 122 | "requires": { 123 | "@xtuc/long": "4.2.2" 124 | } 125 | }, 126 | "@webassemblyjs/utf8": { 127 | "version": "1.11.0", 128 | "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", 129 | "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==", 130 | "dev": true 131 | }, 132 | "@webassemblyjs/wasm-edit": { 133 | "version": "1.11.0", 134 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", 135 | "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", 136 | "dev": true, 137 | "requires": { 138 | "@webassemblyjs/ast": "1.11.0", 139 | "@webassemblyjs/helper-buffer": "1.11.0", 140 | "@webassemblyjs/helper-wasm-bytecode": "1.11.0", 141 | "@webassemblyjs/helper-wasm-section": "1.11.0", 142 | "@webassemblyjs/wasm-gen": "1.11.0", 143 | "@webassemblyjs/wasm-opt": "1.11.0", 144 | "@webassemblyjs/wasm-parser": "1.11.0", 145 | "@webassemblyjs/wast-printer": "1.11.0" 146 | } 147 | }, 148 | "@webassemblyjs/wasm-gen": { 149 | "version": "1.11.0", 150 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", 151 | "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", 152 | "dev": true, 153 | "requires": { 154 | "@webassemblyjs/ast": "1.11.0", 155 | "@webassemblyjs/helper-wasm-bytecode": "1.11.0", 156 | "@webassemblyjs/ieee754": "1.11.0", 157 | "@webassemblyjs/leb128": "1.11.0", 158 | "@webassemblyjs/utf8": "1.11.0" 159 | } 160 | }, 161 | "@webassemblyjs/wasm-opt": { 162 | "version": "1.11.0", 163 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", 164 | "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", 165 | "dev": true, 166 | "requires": { 167 | "@webassemblyjs/ast": "1.11.0", 168 | "@webassemblyjs/helper-buffer": "1.11.0", 169 | "@webassemblyjs/wasm-gen": "1.11.0", 170 | "@webassemblyjs/wasm-parser": "1.11.0" 171 | } 172 | }, 173 | "@webassemblyjs/wasm-parser": { 174 | "version": "1.11.0", 175 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", 176 | "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", 177 | "dev": true, 178 | "requires": { 179 | "@webassemblyjs/ast": "1.11.0", 180 | "@webassemblyjs/helper-api-error": "1.11.0", 181 | "@webassemblyjs/helper-wasm-bytecode": "1.11.0", 182 | "@webassemblyjs/ieee754": "1.11.0", 183 | "@webassemblyjs/leb128": "1.11.0", 184 | "@webassemblyjs/utf8": "1.11.0" 185 | } 186 | }, 187 | "@webassemblyjs/wast-printer": { 188 | "version": "1.11.0", 189 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", 190 | "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", 191 | "dev": true, 192 | "requires": { 193 | "@webassemblyjs/ast": "1.11.0", 194 | "@xtuc/long": "4.2.2" 195 | } 196 | }, 197 | "@webpack-cli/configtest": { 198 | "version": "1.0.2", 199 | "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.2.tgz", 200 | "integrity": "sha512-3OBzV2fBGZ5TBfdW50cha1lHDVf9vlvRXnjpVbJBa20pSZQaSkMJZiwA8V2vD9ogyeXn8nU5s5A6mHyf5jhMzA==", 201 | "dev": true 202 | }, 203 | "@webpack-cli/info": { 204 | "version": "1.2.3", 205 | "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.3.tgz", 206 | "integrity": "sha512-lLek3/T7u40lTqzCGpC6CAbY6+vXhdhmwFRxZLMnRm6/sIF/7qMpT8MocXCRQfz0JAh63wpbXLMnsQ5162WS7Q==", 207 | "dev": true, 208 | "requires": { 209 | "envinfo": "^7.7.3" 210 | } 211 | }, 212 | "@webpack-cli/serve": { 213 | "version": "1.3.1", 214 | "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.3.1.tgz", 215 | "integrity": "sha512-0qXvpeYO6vaNoRBI52/UsbcaBydJCggoBBnIo/ovQQdn6fug0BgwsjorV1hVS7fMqGVTZGcVxv8334gjmbj5hw==", 216 | "dev": true 217 | }, 218 | "@xtuc/ieee754": { 219 | "version": "1.2.0", 220 | "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", 221 | "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", 222 | "dev": true 223 | }, 224 | "@xtuc/long": { 225 | "version": "4.2.2", 226 | "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", 227 | "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", 228 | "dev": true 229 | }, 230 | "acorn": { 231 | "version": "8.1.0", 232 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz", 233 | "integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==", 234 | "dev": true 235 | }, 236 | "ajv": { 237 | "version": "6.12.6", 238 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 239 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 240 | "dev": true, 241 | "requires": { 242 | "fast-deep-equal": "^3.1.1", 243 | "fast-json-stable-stringify": "^2.0.0", 244 | "json-schema-traverse": "^0.4.1", 245 | "uri-js": "^4.2.2" 246 | } 247 | }, 248 | "ajv-keywords": { 249 | "version": "3.5.2", 250 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", 251 | "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", 252 | "dev": true 253 | }, 254 | "ansi-colors": { 255 | "version": "4.1.1", 256 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 257 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 258 | "dev": true 259 | }, 260 | "browserslist": { 261 | "version": "4.16.3", 262 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", 263 | "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", 264 | "dev": true, 265 | "requires": { 266 | "caniuse-lite": "^1.0.30001181", 267 | "colorette": "^1.2.1", 268 | "electron-to-chromium": "^1.3.649", 269 | "escalade": "^3.1.1", 270 | "node-releases": "^1.1.70" 271 | } 272 | }, 273 | "buffer-from": { 274 | "version": "1.1.1", 275 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 276 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 277 | "dev": true 278 | }, 279 | "caniuse-lite": { 280 | "version": "1.0.30001208", 281 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz", 282 | "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==", 283 | "dev": true 284 | }, 285 | "chrome-trace-event": { 286 | "version": "1.0.3", 287 | "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", 288 | "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", 289 | "dev": true 290 | }, 291 | "clone-deep": { 292 | "version": "4.0.1", 293 | "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", 294 | "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", 295 | "dev": true, 296 | "requires": { 297 | "is-plain-object": "^2.0.4", 298 | "kind-of": "^6.0.2", 299 | "shallow-clone": "^3.0.0" 300 | } 301 | }, 302 | "colorette": { 303 | "version": "1.2.2", 304 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", 305 | "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", 306 | "dev": true 307 | }, 308 | "commander": { 309 | "version": "2.20.3", 310 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 311 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 312 | "dev": true 313 | }, 314 | "cross-spawn": { 315 | "version": "7.0.3", 316 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 317 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 318 | "dev": true, 319 | "requires": { 320 | "path-key": "^3.1.0", 321 | "shebang-command": "^2.0.0", 322 | "which": "^2.0.1" 323 | } 324 | }, 325 | "electron-to-chromium": { 326 | "version": "1.3.712", 327 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.712.tgz", 328 | "integrity": "sha512-3kRVibBeCM4vsgoHHGKHmPocLqtFAGTrebXxxtgKs87hNUzXrX2NuS3jnBys7IozCnw7viQlozxKkmty2KNfrw==", 329 | "dev": true 330 | }, 331 | "enhanced-resolve": { 332 | "version": "5.7.0", 333 | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", 334 | "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", 335 | "dev": true, 336 | "requires": { 337 | "graceful-fs": "^4.2.4", 338 | "tapable": "^2.2.0" 339 | } 340 | }, 341 | "enquirer": { 342 | "version": "2.3.6", 343 | "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", 344 | "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", 345 | "dev": true, 346 | "requires": { 347 | "ansi-colors": "^4.1.1" 348 | } 349 | }, 350 | "envinfo": { 351 | "version": "7.8.1", 352 | "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", 353 | "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", 354 | "dev": true 355 | }, 356 | "es-module-lexer": { 357 | "version": "0.4.1", 358 | "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", 359 | "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==", 360 | "dev": true 361 | }, 362 | "escalade": { 363 | "version": "3.1.1", 364 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 365 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 366 | "dev": true 367 | }, 368 | "eslint-scope": { 369 | "version": "5.1.1", 370 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", 371 | "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", 372 | "dev": true, 373 | "requires": { 374 | "esrecurse": "^4.3.0", 375 | "estraverse": "^4.1.1" 376 | } 377 | }, 378 | "esrecurse": { 379 | "version": "4.3.0", 380 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 381 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 382 | "dev": true, 383 | "requires": { 384 | "estraverse": "^5.2.0" 385 | }, 386 | "dependencies": { 387 | "estraverse": { 388 | "version": "5.2.0", 389 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", 390 | "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", 391 | "dev": true 392 | } 393 | } 394 | }, 395 | "estraverse": { 396 | "version": "4.3.0", 397 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 398 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 399 | "dev": true 400 | }, 401 | "events": { 402 | "version": "3.3.0", 403 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 404 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 405 | "dev": true 406 | }, 407 | "execa": { 408 | "version": "5.0.0", 409 | "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", 410 | "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", 411 | "dev": true, 412 | "requires": { 413 | "cross-spawn": "^7.0.3", 414 | "get-stream": "^6.0.0", 415 | "human-signals": "^2.1.0", 416 | "is-stream": "^2.0.0", 417 | "merge-stream": "^2.0.0", 418 | "npm-run-path": "^4.0.1", 419 | "onetime": "^5.1.2", 420 | "signal-exit": "^3.0.3", 421 | "strip-final-newline": "^2.0.0" 422 | } 423 | }, 424 | "fast-deep-equal": { 425 | "version": "3.1.3", 426 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 427 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 428 | "dev": true 429 | }, 430 | "fast-json-stable-stringify": { 431 | "version": "2.1.0", 432 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 433 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 434 | "dev": true 435 | }, 436 | "fastest-levenshtein": { 437 | "version": "1.0.12", 438 | "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", 439 | "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", 440 | "dev": true 441 | }, 442 | "find-up": { 443 | "version": "4.1.0", 444 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 445 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 446 | "dev": true, 447 | "requires": { 448 | "locate-path": "^5.0.0", 449 | "path-exists": "^4.0.0" 450 | } 451 | }, 452 | "function-bind": { 453 | "version": "1.1.1", 454 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 455 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 456 | "dev": true 457 | }, 458 | "get-stream": { 459 | "version": "6.0.0", 460 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", 461 | "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", 462 | "dev": true 463 | }, 464 | "glob-to-regexp": { 465 | "version": "0.4.1", 466 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 467 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 468 | "dev": true 469 | }, 470 | "graceful-fs": { 471 | "version": "4.2.6", 472 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 473 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", 474 | "dev": true 475 | }, 476 | "has": { 477 | "version": "1.0.3", 478 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 479 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 480 | "dev": true, 481 | "requires": { 482 | "function-bind": "^1.1.1" 483 | } 484 | }, 485 | "has-flag": { 486 | "version": "4.0.0", 487 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 488 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 489 | "dev": true 490 | }, 491 | "human-signals": { 492 | "version": "2.1.0", 493 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", 494 | "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", 495 | "dev": true 496 | }, 497 | "import-local": { 498 | "version": "3.0.2", 499 | "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", 500 | "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", 501 | "dev": true, 502 | "requires": { 503 | "pkg-dir": "^4.2.0", 504 | "resolve-cwd": "^3.0.0" 505 | } 506 | }, 507 | "interpret": { 508 | "version": "2.2.0", 509 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", 510 | "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", 511 | "dev": true 512 | }, 513 | "is-core-module": { 514 | "version": "2.2.0", 515 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", 516 | "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", 517 | "dev": true, 518 | "requires": { 519 | "has": "^1.0.3" 520 | } 521 | }, 522 | "is-plain-object": { 523 | "version": "2.0.4", 524 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 525 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 526 | "dev": true, 527 | "requires": { 528 | "isobject": "^3.0.1" 529 | } 530 | }, 531 | "is-stream": { 532 | "version": "2.0.0", 533 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 534 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 535 | "dev": true 536 | }, 537 | "isexe": { 538 | "version": "2.0.0", 539 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 540 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 541 | "dev": true 542 | }, 543 | "isobject": { 544 | "version": "3.0.1", 545 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 546 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 547 | "dev": true 548 | }, 549 | "jest-worker": { 550 | "version": "26.6.2", 551 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", 552 | "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", 553 | "dev": true, 554 | "requires": { 555 | "@types/node": "*", 556 | "merge-stream": "^2.0.0", 557 | "supports-color": "^7.0.0" 558 | } 559 | }, 560 | "json-parse-better-errors": { 561 | "version": "1.0.2", 562 | "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", 563 | "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", 564 | "dev": true 565 | }, 566 | "json-schema-traverse": { 567 | "version": "0.4.1", 568 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 569 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 570 | "dev": true 571 | }, 572 | "kind-of": { 573 | "version": "6.0.3", 574 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 575 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 576 | "dev": true 577 | }, 578 | "loader-runner": { 579 | "version": "4.2.0", 580 | "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", 581 | "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", 582 | "dev": true 583 | }, 584 | "locate-path": { 585 | "version": "5.0.0", 586 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 587 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 588 | "dev": true, 589 | "requires": { 590 | "p-locate": "^4.1.0" 591 | } 592 | }, 593 | "merge-stream": { 594 | "version": "2.0.0", 595 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 596 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 597 | "dev": true 598 | }, 599 | "mime-db": { 600 | "version": "1.47.0", 601 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", 602 | "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", 603 | "dev": true 604 | }, 605 | "mime-types": { 606 | "version": "2.1.30", 607 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", 608 | "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", 609 | "dev": true, 610 | "requires": { 611 | "mime-db": "1.47.0" 612 | } 613 | }, 614 | "mimic-fn": { 615 | "version": "2.1.0", 616 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 617 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 618 | "dev": true 619 | }, 620 | "neo-async": { 621 | "version": "2.6.2", 622 | "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", 623 | "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", 624 | "dev": true 625 | }, 626 | "node-releases": { 627 | "version": "1.1.71", 628 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", 629 | "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", 630 | "dev": true 631 | }, 632 | "npm-run-path": { 633 | "version": "4.0.1", 634 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 635 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 636 | "dev": true, 637 | "requires": { 638 | "path-key": "^3.0.0" 639 | } 640 | }, 641 | "onetime": { 642 | "version": "5.1.2", 643 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 644 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 645 | "dev": true, 646 | "requires": { 647 | "mimic-fn": "^2.1.0" 648 | } 649 | }, 650 | "p-limit": { 651 | "version": "3.1.0", 652 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 653 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 654 | "dev": true, 655 | "requires": { 656 | "yocto-queue": "^0.1.0" 657 | } 658 | }, 659 | "p-locate": { 660 | "version": "4.1.0", 661 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 662 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 663 | "dev": true, 664 | "requires": { 665 | "p-limit": "^2.2.0" 666 | }, 667 | "dependencies": { 668 | "p-limit": { 669 | "version": "2.3.0", 670 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 671 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 672 | "dev": true, 673 | "requires": { 674 | "p-try": "^2.0.0" 675 | } 676 | } 677 | } 678 | }, 679 | "p-try": { 680 | "version": "2.2.0", 681 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 682 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 683 | "dev": true 684 | }, 685 | "path-exists": { 686 | "version": "4.0.0", 687 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 688 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 689 | "dev": true 690 | }, 691 | "path-key": { 692 | "version": "3.1.1", 693 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 694 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 695 | "dev": true 696 | }, 697 | "path-parse": { 698 | "version": "1.0.6", 699 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 700 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 701 | "dev": true 702 | }, 703 | "pkg-dir": { 704 | "version": "4.2.0", 705 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 706 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 707 | "dev": true, 708 | "requires": { 709 | "find-up": "^4.0.0" 710 | } 711 | }, 712 | "punycode": { 713 | "version": "2.1.1", 714 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 715 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 716 | "dev": true 717 | }, 718 | "randombytes": { 719 | "version": "2.1.0", 720 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 721 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 722 | "dev": true, 723 | "requires": { 724 | "safe-buffer": "^5.1.0" 725 | } 726 | }, 727 | "rechoir": { 728 | "version": "0.7.0", 729 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz", 730 | "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==", 731 | "dev": true, 732 | "requires": { 733 | "resolve": "^1.9.0" 734 | } 735 | }, 736 | "resolve": { 737 | "version": "1.20.0", 738 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 739 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 740 | "dev": true, 741 | "requires": { 742 | "is-core-module": "^2.2.0", 743 | "path-parse": "^1.0.6" 744 | } 745 | }, 746 | "resolve-cwd": { 747 | "version": "3.0.0", 748 | "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", 749 | "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", 750 | "dev": true, 751 | "requires": { 752 | "resolve-from": "^5.0.0" 753 | } 754 | }, 755 | "resolve-from": { 756 | "version": "5.0.0", 757 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", 758 | "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", 759 | "dev": true 760 | }, 761 | "safe-buffer": { 762 | "version": "5.2.1", 763 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 764 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 765 | "dev": true 766 | }, 767 | "schema-utils": { 768 | "version": "3.0.0", 769 | "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", 770 | "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", 771 | "dev": true, 772 | "requires": { 773 | "@types/json-schema": "^7.0.6", 774 | "ajv": "^6.12.5", 775 | "ajv-keywords": "^3.5.2" 776 | } 777 | }, 778 | "serialize-javascript": { 779 | "version": "5.0.1", 780 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", 781 | "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", 782 | "dev": true, 783 | "requires": { 784 | "randombytes": "^2.1.0" 785 | } 786 | }, 787 | "shallow-clone": { 788 | "version": "3.0.1", 789 | "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", 790 | "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", 791 | "dev": true, 792 | "requires": { 793 | "kind-of": "^6.0.2" 794 | } 795 | }, 796 | "shebang-command": { 797 | "version": "2.0.0", 798 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 799 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 800 | "dev": true, 801 | "requires": { 802 | "shebang-regex": "^3.0.0" 803 | } 804 | }, 805 | "shebang-regex": { 806 | "version": "3.0.0", 807 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 808 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 809 | "dev": true 810 | }, 811 | "signal-exit": { 812 | "version": "3.0.3", 813 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 814 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 815 | "dev": true 816 | }, 817 | "source-list-map": { 818 | "version": "2.0.1", 819 | "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", 820 | "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", 821 | "dev": true 822 | }, 823 | "source-map": { 824 | "version": "0.6.1", 825 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 826 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 827 | "dev": true 828 | }, 829 | "source-map-support": { 830 | "version": "0.5.19", 831 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 832 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 833 | "dev": true, 834 | "requires": { 835 | "buffer-from": "^1.0.0", 836 | "source-map": "^0.6.0" 837 | } 838 | }, 839 | "strip-final-newline": { 840 | "version": "2.0.0", 841 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 842 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 843 | "dev": true 844 | }, 845 | "supports-color": { 846 | "version": "7.2.0", 847 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 848 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 849 | "dev": true, 850 | "requires": { 851 | "has-flag": "^4.0.0" 852 | } 853 | }, 854 | "tapable": { 855 | "version": "2.2.0", 856 | "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", 857 | "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", 858 | "dev": true 859 | }, 860 | "terser": { 861 | "version": "5.6.1", 862 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.1.tgz", 863 | "integrity": "sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==", 864 | "dev": true, 865 | "requires": { 866 | "commander": "^2.20.0", 867 | "source-map": "~0.7.2", 868 | "source-map-support": "~0.5.19" 869 | }, 870 | "dependencies": { 871 | "source-map": { 872 | "version": "0.7.3", 873 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 874 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 875 | "dev": true 876 | } 877 | } 878 | }, 879 | "terser-webpack-plugin": { 880 | "version": "5.1.1", 881 | "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz", 882 | "integrity": "sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==", 883 | "dev": true, 884 | "requires": { 885 | "jest-worker": "^26.6.2", 886 | "p-limit": "^3.1.0", 887 | "schema-utils": "^3.0.0", 888 | "serialize-javascript": "^5.0.1", 889 | "source-map": "^0.6.1", 890 | "terser": "^5.5.1" 891 | } 892 | }, 893 | "uri-js": { 894 | "version": "4.4.1", 895 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 896 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 897 | "dev": true, 898 | "requires": { 899 | "punycode": "^2.1.0" 900 | } 901 | }, 902 | "v8-compile-cache": { 903 | "version": "2.3.0", 904 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", 905 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", 906 | "dev": true 907 | }, 908 | "watchpack": { 909 | "version": "2.1.1", 910 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz", 911 | "integrity": "sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw==", 912 | "dev": true, 913 | "requires": { 914 | "glob-to-regexp": "^0.4.1", 915 | "graceful-fs": "^4.1.2" 916 | } 917 | }, 918 | "webpack": { 919 | "version": "5.31.2", 920 | "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.31.2.tgz", 921 | "integrity": "sha512-0bCQe4ybo7T5Z0SC5axnIAH+1WuIdV4FwLYkaAlLtvfBhIx8bPS48WHTfiRZS1VM+pSiYt7e/rgLs3gLrH82lQ==", 922 | "dev": true, 923 | "requires": { 924 | "@types/eslint-scope": "^3.7.0", 925 | "@types/estree": "^0.0.46", 926 | "@webassemblyjs/ast": "1.11.0", 927 | "@webassemblyjs/wasm-edit": "1.11.0", 928 | "@webassemblyjs/wasm-parser": "1.11.0", 929 | "acorn": "^8.0.4", 930 | "browserslist": "^4.14.5", 931 | "chrome-trace-event": "^1.0.2", 932 | "enhanced-resolve": "^5.7.0", 933 | "es-module-lexer": "^0.4.0", 934 | "eslint-scope": "^5.1.1", 935 | "events": "^3.2.0", 936 | "glob-to-regexp": "^0.4.1", 937 | "graceful-fs": "^4.2.4", 938 | "json-parse-better-errors": "^1.0.2", 939 | "loader-runner": "^4.2.0", 940 | "mime-types": "^2.1.27", 941 | "neo-async": "^2.6.2", 942 | "schema-utils": "^3.0.0", 943 | "tapable": "^2.1.1", 944 | "terser-webpack-plugin": "^5.1.1", 945 | "watchpack": "^2.0.0", 946 | "webpack-sources": "^2.1.1" 947 | } 948 | }, 949 | "webpack-cli": { 950 | "version": "4.6.0", 951 | "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.6.0.tgz", 952 | "integrity": "sha512-9YV+qTcGMjQFiY7Nb1kmnupvb1x40lfpj8pwdO/bom+sQiP4OBMKjHq29YQrlDWDPZO9r/qWaRRywKaRDKqBTA==", 953 | "dev": true, 954 | "requires": { 955 | "@discoveryjs/json-ext": "^0.5.0", 956 | "@webpack-cli/configtest": "^1.0.2", 957 | "@webpack-cli/info": "^1.2.3", 958 | "@webpack-cli/serve": "^1.3.1", 959 | "colorette": "^1.2.1", 960 | "commander": "^7.0.0", 961 | "enquirer": "^2.3.6", 962 | "execa": "^5.0.0", 963 | "fastest-levenshtein": "^1.0.12", 964 | "import-local": "^3.0.2", 965 | "interpret": "^2.2.0", 966 | "rechoir": "^0.7.0", 967 | "v8-compile-cache": "^2.2.0", 968 | "webpack-merge": "^5.7.3" 969 | }, 970 | "dependencies": { 971 | "commander": { 972 | "version": "7.2.0", 973 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 974 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 975 | "dev": true 976 | } 977 | } 978 | }, 979 | "webpack-merge": { 980 | "version": "5.7.3", 981 | "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", 982 | "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", 983 | "dev": true, 984 | "requires": { 985 | "clone-deep": "^4.0.1", 986 | "wildcard": "^2.0.0" 987 | } 988 | }, 989 | "webpack-sources": { 990 | "version": "2.2.0", 991 | "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.2.0.tgz", 992 | "integrity": "sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==", 993 | "dev": true, 994 | "requires": { 995 | "source-list-map": "^2.0.1", 996 | "source-map": "^0.6.1" 997 | } 998 | }, 999 | "which": { 1000 | "version": "2.0.2", 1001 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1002 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1003 | "dev": true, 1004 | "requires": { 1005 | "isexe": "^2.0.0" 1006 | } 1007 | }, 1008 | "wildcard": { 1009 | "version": "2.0.0", 1010 | "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", 1011 | "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", 1012 | "dev": true 1013 | }, 1014 | "yocto-queue": { 1015 | "version": "0.1.0", 1016 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1017 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1018 | "dev": true 1019 | } 1020 | } 1021 | } 1022 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmap", 3 | "version": "1.2.0", 4 | "description": "A Valheim map viewer for the browser", 5 | "scripts": { 6 | "build": "webpack ./WebMap/web-src/index.js -o ./WebMap/web --mode development", 7 | "build-prod": "webpack ./WebMap/web-src/index.js -o ./WebMap/web --mode production" 8 | }, 9 | "author": "Kyle Paulsen ", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "webpack": "^5.31.2", 13 | "webpack-cli": "^4.6.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylepaulsen/ValheimWebMap/df2920106aac50c226e310e7ba528ce723be4fb1/thumb.png --------------------------------------------------------------------------------