diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index b570fa6..4314e84 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -17,6 +17,7 @@ services: volumes: - ixion-dev-node_modules:/workspace/client/node_modules - ../client:/workspace/client:cached + - ../api:/workspace/api:cached ports: - 3000:3000 depends_on: @@ -32,6 +33,7 @@ services: volumes: - ixion-dev-static:/workspace/api/static - ../api:/workspace/api:cached + - ../client:/workspace/client:cached ports: - 8000:8000 depends_on: diff --git a/api/api/urls.py b/api/api/urls.py index 8d6aec3..62f60ad 100644 --- a/api/api/urls.py +++ b/api/api/urls.py @@ -18,12 +18,13 @@ Including another URLconf from django.contrib import admin from django.urls import path, include from rest_framework import routers -from v1.views import server +from v1.views.server import ServerViewSet, ServerDetailsViewSet router = routers.DefaultRouter() -router.register(r"servers", server.ServerViewSet, "v1") +router.register(r"servers", ServerViewSet, basename="servers") +router.register(r"server", ServerDetailsViewSet, basename="server") urlpatterns = [ path("admin/", admin.site.urls), - path("v1/", include(router.urls)), + path("", include(router.urls)), ] diff --git a/client/src/data/defaultLsbSettings.json b/api/defaultLsbSettings.json similarity index 100% rename from client/src/data/defaultLsbSettings.json rename to api/defaultLsbSettings.json diff --git a/api/docker-compose.yml b/api/docker-compose.yml index ebc0731..0701ec4 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -5,8 +5,9 @@ services: restart: always volumes: - .:/app - ports: - - 8000:8000 + - static:/app/static + expose: + - 8000 depends_on: - redis celery: @@ -28,3 +29,14 @@ services: redis: image: redis:alpine restart: always + nginx: + image: nginx + ports: + - 9969:80 + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ./static:/app/static + depends_on: + - api +volumes: + static: diff --git a/api/nginx.conf b/api/nginx.conf new file mode 100644 index 0000000..2fa7a37 --- /dev/null +++ b/api/nginx.conf @@ -0,0 +1,20 @@ +upstream ixion_api { + server api:8000; +} + +server { + + listen 80; + + location / { + proxy_pass http://ixion_api; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } + + location /static/ { + alias /app/static/; + } + +} diff --git a/api/v1/migrations/0001_initial.py b/api/v1/migrations/0001_initial.py new file mode 100644 index 0000000..560fb00 --- /dev/null +++ b/api/v1/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.4 on 2024-04-15 16:13 + +import v1.models.server +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Server", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(editable=False, max_length=255, null=True)), + ( + "url", + models.CharField( + max_length=255, + validators=[v1.models.server.OptionalSchemeURLValidator()], + ), + ), + ("max_level", models.IntegerField(editable=False, null=True)), + ("settings", models.JSONField(editable=False, null=True)), + ("customizations", models.JSONField(editable=False, null=True)), + ("login_limit", models.IntegerField(editable=False, null=True)), + ("active_sessions", models.IntegerField(editable=False, null=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("inactivity_counter", models.IntegerField(editable=False, null=True)), + ], + ), + ] diff --git a/api/v1/models/server.py b/api/v1/models/server.py index 0ffbb6e..b05c0fe 100644 --- a/api/v1/models/server.py +++ b/api/v1/models/server.py @@ -1,4 +1,6 @@ +import json import os +import socket import requests from django.db import models from django.core.validators import URLValidator @@ -6,13 +8,23 @@ from django.core.exceptions import ValidationError from urllib.parse import urlparse from v1.common.logging import logger - +# Required and recommended settings used to build the initial client server card required_settings = [ "MAIN.SERVER_NAME", "MAIN.MAX_LEVEL", "LOGIN.LOGIN_LIMIT", ] +recommended_settings = [ + "MAIN.ENABLE_TRUST_CASTING", + "MAP.LEVEL_SYNC_ENABLE", + "LOGIN.RISE_OF_ZILART", + "LOGIN.CHAINS_OF_PROMATHIA", + "LOGIN.TREASURES_OF_AHT_URGHAN", + "LOGIN.WINGS_OF_THE_GODDESS", + "LOGIN.SEEKERS_OF_ADOULIN", +] + class OptionalSchemeURLValidator(URLValidator): def __call__(self, value): @@ -23,12 +35,16 @@ class OptionalSchemeURLValidator(URLValidator): class Server(models.Model): + name = models.CharField(max_length=255, null=True, editable=False) url = models.CharField( - max_length=400, + max_length=255, null=False, validators=[OptionalSchemeURLValidator()], ) + max_level = models.IntegerField(null=True, editable=False) settings = models.JSONField(null=True, editable=False) + customizations = models.JSONField(null=True, editable=False) + login_limit = models.IntegerField(null=True, editable=False) active_sessions = models.IntegerField(null=True, editable=False) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) @@ -73,26 +89,59 @@ class Server(models.Model): def parse_server_api(self): try: # Request server settings from API - response = requests.get(f"http://{self.url}/api/settings") - response.raise_for_status() # Raise an exception for HTTP errors (4xx and 5xx) - json_data = response.json() - - # Validate the JSON structure - if not isinstance(json_data, dict): + response = requests.get(f"http://{self.url}/api/settings", timeout=5) + response.raise_for_status() + server_settings = response.json() + if not isinstance(server_settings, dict): return False + # Check for required settings for setting in required_settings: - if setting not in json_data: + if setting not in server_settings: return False + self.name = server_settings["MAIN.SERVER_NAME"] + self.max_level = server_settings["MAIN.MAX_LEVEL"] + self.login_limit = server_settings["LOGIN.LOGIN_LIMIT"] - self.settings = json_data + # Check customizations + with open("defaultLsbSettings.json", "r") as default_settings_file: + default_settings = json.load(default_settings_file) + customizations = {} + + customizations["LOGIN.CLIENT_VER"] = server_settings["LOGIN.CLIENT_VER"] + + for key, value in server_settings.items(): + if key not in required_settings and ( + key in recommended_settings + or key not in default_settings + or default_settings[key] != value + ): + customizations[key] = value + + self.customizations = customizations + self.settings = server_settings # Request the active session count from API response = requests.get(f"http://{self.url}/api/sessions") - response.raise_for_status() # Raise an exception for HTTP errors (4xx and 5xx) + response.raise_for_status() session_count = response.text if session_count.isdigit(): self.active_sessions = int(session_count) + + # Test other server ports (you can actually change all of these?) + # ports_to_check = [ + # 54230, # NETWORK.LOGIN_DATA_PORT, NETWORK.MAP_PORT + # 54231, # NETWORK.LOGIN_AUTH_PORT + # 54001, # NETWORK.LOGIN_VIEW_PORT + # ] + # for port in ports_to_check: + # try: + # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + # s.settimeout(5) + # s.connect((self.url, port)) + # except socket.error: + # return False + return True except requests.RequestException: diff --git a/api/v1/serializers/server.py b/api/v1/serializers/server.py index f8dffd5..992db72 100644 --- a/api/v1/serializers/server.py +++ b/api/v1/serializers/server.py @@ -6,10 +6,26 @@ from django.core.exceptions import ValidationError class ServerSerializer(serializers.ModelSerializer): class Meta: model = Server - fields = "__all__" + fields = [ + "id", + "name", + "url", + "max_level", + "customizations", + "login_limit", + "active_sessions", + "updated", + "inactivity_counter", + ] def create(self, validated_data): try: return super().create(validated_data) except ValidationError as e: raise serializers.ValidationError({"url": e.messages}) + + +class ServerDetailsSerializer(serializers.ModelSerializer): + class Meta: + model = Server + fields = "__all__" diff --git a/api/v1/views/server.py b/api/v1/views/server.py index d4addfd..eaf303e 100644 --- a/api/v1/views/server.py +++ b/api/v1/views/server.py @@ -1,19 +1,9 @@ from rest_framework import viewsets, mixins from django.db.models.fields.json import KT -from v1.serializers.server import ServerSerializer +from v1.serializers.server import ServerDetailsSerializer, ServerSerializer from v1.models.server import Server -class ServerView( - mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet, -): - serializer_class = ServerSerializer - queryset = Server.objects.all() - - class ServerViewSet( mixins.CreateModelMixin, mixins.RetrieveModelMixin, @@ -28,14 +18,6 @@ class ServerViewSet( "annotate_field": "settings__MAIN.SERVER_NAME", "filter_type": "startswith", }, - "level_cap_min": { - "annotate_field": "settings__MAIN.MAX_LEVEL", - "filter_type": "gte", - }, - "level_cap_max": { - "annotate_field": "settings__MAIN.MAX_LEVEL", - "filter_type": "lte", - }, } def get_queryset(self): @@ -58,3 +40,11 @@ class ServerViewSet( queryset._result_cache = None return queryset + + +class ServerDetailsViewSet( + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): + queryset = Server.objects.all() + serializer_class = ServerDetailsSerializer diff --git a/client/index.html b/client/index.html index a611cce..1385ce5 100644 --- a/client/index.html +++ b/client/index.html @@ -16,7 +16,12 @@ href="https://fonts.googleapis.com/icon?family=Material+Icons" /> - IXION - FFXI Private Server Directory + + + + + + IXION - Private Servers
diff --git a/client/public/android-chrome-192x192.png b/client/public/android-chrome-192x192.png new file mode 100644 index 0000000..2df181e Binary files /dev/null and b/client/public/android-chrome-192x192.png differ diff --git a/client/public/android-chrome-512x512.png b/client/public/android-chrome-512x512.png new file mode 100644 index 0000000..fa29c0a Binary files /dev/null and b/client/public/android-chrome-512x512.png differ diff --git a/client/public/apple-touch-icon.png b/client/public/apple-touch-icon.png new file mode 100644 index 0000000..f4d1cd5 Binary files /dev/null and b/client/public/apple-touch-icon.png differ diff --git a/client/public/favicon-16x16.png b/client/public/favicon-16x16.png new file mode 100644 index 0000000..a380509 Binary files /dev/null and b/client/public/favicon-16x16.png differ diff --git a/client/public/favicon-32x32.png b/client/public/favicon-32x32.png new file mode 100644 index 0000000..246408e Binary files /dev/null and b/client/public/favicon-32x32.png differ diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000..47b1197 Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/public/site.webmanifest b/client/public/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/client/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/client/src/apiUtil.tsx b/client/src/apiUtil.tsx index 989accb..2b168e2 100644 --- a/client/src/apiUtil.tsx +++ b/client/src/apiUtil.tsx @@ -23,7 +23,7 @@ export const fetchDemo = async () => { export const fetchData = async (queryParams = {}) => { const queryString = new URLSearchParams(queryParams).toString(); - const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/v1/servers/${queryString ? `?${queryString}` : ''}`; + const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/servers/${queryString ? `?${queryString}` : ''}`; try { const response = await fetch(url, { signal: AbortSignal.timeout(5000), @@ -47,7 +47,7 @@ export const fetchData = async (queryParams = {}) => { export const postData = async (inputText: string) => { try { const response = await fetch( - `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/v1/servers/`, + `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/servers/`, { method: 'POST', headers: { diff --git a/client/src/components/Accordion.tsx b/client/src/components/Accordion.tsx index 5024e83..f1838fb 100644 --- a/client/src/components/Accordion.tsx +++ b/client/src/components/Accordion.tsx @@ -30,7 +30,7 @@ export const AccordionSummary = styled((props: AccordionSummaryProps) => ( '& .MuiAccordionSummary-content': { margin: theme.spacing(0), }, - boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', // Adjust the size and opacity as needed + boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', minHeight: '32px', })); diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 1a01516..baf1410 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -75,7 +75,6 @@ export default function Header({ sx={{ color: 'inherit', textDecoration: 'none', - userSelect: 'none', flexGrow: 1, }} > diff --git a/client/src/components/ServerCard.tsx b/client/src/components/ServerCard.tsx index d377e7a..4e08755 100644 --- a/client/src/components/ServerCard.tsx +++ b/client/src/components/ServerCard.tsx @@ -1,4 +1,6 @@ import { + Check, + Close, ContentCopy, ExpandMore, Launch, @@ -10,13 +12,9 @@ import { Box, Card, CardContent, + Chip, Divider, IconButton, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, ToggleButton, ToggleButtonGroup, Tooltip, @@ -25,7 +23,11 @@ import { } from '@mui/material'; import { useState } from 'react'; import { Link } from 'react-router-dom'; -import ServerData, { ServerSettingsInfo } from '../data/ServerData'; +import ServerData, { + ServerSetting, + ServerSettingsInfo, +} from '../data/ServerData'; +import CopyImageIcon from '../images/copy-image.png'; import { Accordion, AccordionDetails, AccordionSummary } from './Accordion'; export default function ServerCard({ server }: { server: ServerData }) { @@ -63,15 +65,75 @@ export default function ServerCard({ server }: { server: ServerData }) { }; function formatExternalUrl(url: string): string { - // Check if the URL starts with a valid protocol + // prepend 'https://' to the URL if it's not already there if (!/^https?:\/\//i.test(url)) { - // If not, prepend 'https://' to the URL return `https://${url}`; } - // Otherwise, return the original URL return url; } + const renderSettingsChip = ([key, value]: [ + string, + boolean | string | number, + ]) => { + if (!ServerSettingsInfo[key]) return null; + + const transformValue = ( + v: boolean | string | number, + setting: ServerSetting + ): string | number | boolean => { + return setting.transform?.(v) ?? v; + }; + + let chipValue: string | number | boolean | JSX.Element = transformValue( + value, + ServerSettingsInfo[key] + ); + if (typeof chipValue === 'boolean') { + chipValue = chipValue ? ( + + ) : ( + + ); + } + + return ( + + + ) + } + icon={typeof chipValue === 'object' ? chipValue : undefined} + size="small" + className="m-1 pr-1" + sx={{ + '& .MuiChip-avatar': { + width: 'auto', + marginX: 0, + order: 2, + }, + '& .MuiChip-icon': { + marginX: 0, + order: 2, + }, + '&> .MuiChip-label': { + paddingRight: 0.5, + }, + boxShadow: '0px 3px 3px rgba(0, 0, 0, .25)', + }} + /> + + ); + }; + const expansions = [ }> - {server.settings['LOGIN.MAINT_MODE'] === 1 ? ( + {server.customizations['LOGIN.MAINT_MODE'] === 1 ? ( alpha(theme.palette.text.primary, 0.87)} sx={{ lineHeight: 1.0 }} > - {server.settings['MAIN.SERVER_NAME']} + {server.name} - {typeof server.settings['API.WEBSITE'] === 'string' && - server.settings['API.WEBSITE'] !== '' && ( + {typeof server.customizations['API.WEBSITE'] === 'string' && + server.customizations['API.WEBSITE'] !== '' && ( @@ -241,95 +306,57 @@ export default function ServerCard({ server }: { server: ServerData }) { variant="h5" color={(theme) => theme.palette.text.secondary} > - {`Lv.${server.settings['MAIN.MAX_LEVEL']}`} + {`Lv.${server.max_level}`} - - - - - - - - {`${ServerSettingsInfo['LOGIN.LOGIN_LIMIT'].name}: `} - {server.settings['LOGIN.LOGIN_LIMIT'] === 0 - ? 'unlimited' - : server.settings['LOGIN.LOGIN_LIMIT']} - - - - - - - {`${ServerSettingsInfo['MAIN.ENABLE_TRUST_CASTING'].name}: `} - {server.settings['MAIN.ENABLE_TRUST_CASTING'] === 1 - ? 'Enabled' - : 'Disabled'} - - - - - - - {`${ServerSettingsInfo['MAP.LEVEL_SYNC_ENABLE'].name}: `} - {server.settings['MAP.LEVEL_SYNC_ENABLE'] - ? 'Enabled' - : 'Disabled'} - - - - - *': { borderBottom: 'unset' } }}> - - - - {`${ServerSettingsInfo['MAP.SPEED_MOD'].name}: `} - {typeof server.settings['MAP.SPEED_MOD'] === 'number' - ? (server.settings['MAP.SPEED_MOD'] < 0 ? '' : '+') + - ((50 + server.settings['MAP.SPEED_MOD']) / 50 - 1) * - 100 - : '???'} - % - - - - - -
-
+ {server.customizations['MAIN.SERVER_MESSAGE'] && ( + <> + + + {server.customizations['MAIN.SERVER_MESSAGE']} + + + + + )} + + {Object.entries(server.customizations).map(renderSettingsChip)} +
- + {server.active_sessions} active sessions + {server.login_limit !== 1 && ( + + event.preventDefault()} + /> + + )} - + Updated: {new Date(server.updated).toLocaleString()} diff --git a/client/src/data/ServerData.tsx b/client/src/data/ServerData.tsx index bec582a..7d3a14b 100644 --- a/client/src/data/ServerData.tsx +++ b/client/src/data/ServerData.tsx @@ -1,4 +1,11 @@ -import LsbDefaults from './defaultLsbSettings.json'; +import LsbDefaults from '../../../api/defaultLsbSettings.json'; + +export interface ServerSetting { + name: string; + description: string; + unit?: string; + transform?: (arg: boolean | string | number) => boolean | string | number; +} interface ServerSettings { [key: string]: boolean | string | number; @@ -6,8 +13,11 @@ interface ServerSettings { export default interface ServerData { id: number; + name: string; url: string; - settings: ServerSettings; + max_level: number; + customizations: ServerSettings; + login_limit: number; active_sessions: number; updated: string; inactivity_counter: number; @@ -15,1222 +25,1735 @@ export default interface ServerData { export const DemoServerData: ServerData = { id: 0, + name: 'LandSandBoat Demo', url: 'github.com/LandSandBoat/server', - settings: { + max_level: 99, + customizations: { ...LsbDefaults, - 'MAIN.SERVER_NAME': 'LandSandBoat Demo', 'API.WEBSITE': 'https://landsandboat.github.io/server/', }, + login_limit: 0, active_sessions: 0, updated: new Date().toISOString(), inactivity_counter: 0, }; -export const ServerSettingsInfo: Record< - string, - Record -> = { +export const ServerSettingsInfo: Record = { 'LOGIN.ACCOUNT_CREATION': { name: 'Account Creation', - description: 'Allows account creation via the loader.', - }, - 'LOGIN.A_CRYSTALLINE_PROPHECY': { - name: 'A Crystalline Prophecy', - description: 'Displays "A Crystalline Prophecy" expansion.', - }, - 'LOGIN.A_MOOGLE_KUPOD_ETAT': { - name: "A Moogle Kupo d'Etat", - description: 'Displays "A Moogle Kupo d\'Etat" expansion.', - }, - 'LOGIN.A_SHANTOTTO_ASCENSION': { - name: 'A Shantotto Ascension', - description: 'Displays "A Shantotto Ascension" expansion.', - }, - 'LOGIN.CHAINS_OF_PROMATHIA': { - name: 'Chains of Promathia', - description: 'Displays "Chains of Promathia" expansion.', + description: 'Account creation via the loader.', }, 'LOGIN.CHARACTER_CREATION': { name: 'Character Creation', - description: 'Allows character creation through the lobby.', + description: 'Character creation through the lobby.', }, 'LOGIN.CHARACTER_DELETION': { name: 'Character Deletion', - description: 'Allows character deletion through the lobby.', + description: 'Character deletion through the lobby.', }, 'LOGIN.CLIENT_VER': { name: 'Client Version', description: 'Expected client version.', }, - 'LOGIN.DISABLE_MOB_NPC_CHAR_NAMES': { - name: 'Disable Mob/NPC Character Names', - description: - 'Blocks character creation with names of NPCs and Mobs in the database', - }, - 'LOGIN.HEROES_OF_ABYSSEA': { - name: 'Heroes of Abyssea', - description: 'Displays "Heroes of Abyssea" expansion.', - }, - 'LOGIN.LOGIN_LIMIT': { - name: 'Login Limit', - description: 'Number of simultaneous game sessions per IP.', - }, 'LOGIN.LOG_USER_IP': { - name: 'Logs User IP', - description: 'Logs user IP address to database.', - }, - 'LOGIN.MAINT_MODE': { - name: '', - description: '', - }, - 'LOGIN.MOG_WARDROBE_3': { - name: '', - description: '', - }, - 'LOGIN.MOG_WARDROBE_4': { - name: '', - description: '', - }, - 'LOGIN.MOG_WARDROBE_5': { - name: '', - description: '', - }, - 'LOGIN.MOG_WARDROBE_6': { - name: '', - description: '', - }, - 'LOGIN.MOG_WARDROBE_7': { - name: '', - description: '', - }, - 'LOGIN.MOG_WARDROBE_8': { - name: '', - description: '', - }, - 'LOGIN.RISE_OF_ZILART': { - name: 'Rise of the Zilart', - description: 'Displays "Rise of the Zilart" expansion.', - }, - 'LOGIN.SCARS_OF_ABYSSEA': { - name: 'Scars of Abyssea', - description: 'Displays "Scars of Abyssea" expansion.', - }, - 'LOGIN.SECURE_TOKEN': { - name: '', - description: '', - }, - 'LOGIN.SEEKERS_OF_ADOULIN': { - name: 'Seekers of Adoulin', - description: 'Displays "Seekers of Adoulin" expansion.', - }, - 'LOGIN.TREASURES_OF_AHT_URGHAN': { - name: 'Treasures of Aht Urghan', - description: 'Displays "Treasures of Aht Urghan" expansion.', + name: 'Logs IP', + description: 'User IP address logging.', }, 'LOGIN.VER_LOCK': { - name: 'Version Lock', - description: '0 = disabled; 1 = strict; 2 = greater than or equal', - }, - 'LOGIN.VISIONS_OF_ABYSSEA': { - name: 'Visions of Abyssea', - description: 'Displays "Visions of Abyssea" expansion.', - }, - 'LOGIN.WINGS_OF_THE_GODDESS': { - name: 'Wings of the Goddess', - description: 'Displays "Wings of the Goddess" expansion.', + name: 'Version Locked', + description: 'Requires the exact client version specified.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ABSORB_SPELL_AMOUNT': { - name: '', - description: '', + name: 'Absorb Spell Amount', + description: 'How much of a stat gets absorbed by DRK absorb spells.', }, 'MAIN.ABSORB_SPELL_TICK': { - name: '', - description: '', + name: 'Absorb Spell Tick', + description: 'Duration of 1 absorb spell tick.', + transform: (arg) => { + return `${arg}s`; + }, }, 'MAIN.ABYSSEA_BONUSLIGHT_AMOUNT': { - name: '', - description: '', + name: 'Abyssea Bonus Light', + description: + 'Bonus added to player lights upon entering Abyssea, mainly used during events.', + transform: (arg) => { + return `+${arg}`; + }, }, 'MAIN.ABYSSEA_LIGHTS_DROP_RATE': { - name: '', - description: '', + name: 'Abyssea Lights Drop Rate', + description: 'Rate that mobs drop Abyssea light.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.ACTIVATE_LAMP_TIME': { - name: '', - description: '', + name: 'Nyzul Lamps', + description: 'Time that lamps in Nyzul stay lit.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 1000}s`; + } + return '???'; + }, }, 'MAIN.ADVANCED_JOB_LEVEL': { - name: '', - description: '', + name: 'Advanced Jobs', + description: 'Minimum level to accept advanced job quests.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.AF1_QUEST_LEVEL': { - name: '', - description: '', + name: 'AF1 Quest', + description: 'Minimum level to start AF1 quest.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.AF2_QUEST_LEVEL': { - name: '', - description: '', + name: 'AF2 Quest', + description: 'Minimum level to start AF2 quest.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.AF3_QUEST_LEVEL': { - name: '', - description: '', + name: 'AF3 Quest', + description: 'Minimum level to start AF3 quest.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.ALLOW_MULTIPLE_EXP_RINGS': { - name: '', - description: '', + name: 'Multiple EXP Rings', + description: + 'Removes ownership restrictions on the Chariot, Empress, Emperor Band trio.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ALL_MAPS': { - name: '', - description: '', + name: 'All Maps', + description: 'New characters receive all maps.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.AQUAVEIL_COUNTER': { - name: '', - description: '', + name: 'Aquaveil Counter', + description: + 'Base amount of hits Aquaveil absorbs to prevent spell interrupts.', }, 'MAIN.ASSAULT_MINIMUM': { - name: '', - description: '', + name: 'Assault Minimum', + description: + 'Minimum amount of players required to start an assault mission.', }, 'MAIN.BAYLD_RATE': { - name: '', + name: 'Quest Bayld Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.BETWEEN_2DYNA_WAIT_TIME': { - name: '', - description: '', + name: 'Dynamis Lockout', + description: 'Time before Dynamis re-entry.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue}hr`; + } + return '???'; + }, }, 'MAIN.BIO_OVERWRITE': { - name: '', - description: '', + name: 'Dia Overwrites Bio', + description: 'Dia overwrites same tier Bio.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.BLINK_SHADOWS': { - name: '', - description: '', + name: 'Blink Shadows', + description: 'Number of shadows supplied by Blink spell.', }, 'MAIN.BLUE_POWER': { - name: '', - description: '', + name: 'Blue Magic Power', + description: 'Multiplies damage dealt by Blue Magic.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.BOOK_EXP_RATE': { - name: '', + name: 'FoV/GoV EXP Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.BYPASS_EXP_RING_ONE_PER_WEEK': { - name: '', - description: '', + name: 'Multiple EXP Rings Per Week', + description: 'Bypass the limit of one ring per Conquest Tally week.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.CAPACITY_RATE': { - name: '', + name: 'Quest Capacity Points Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.CAP_CURRENCY_ACCOLADES': { - name: '', - description: '', + name: 'Unity Accolades Cap', + description: 'Unity Accolades currency cap.', }, 'MAIN.CAP_CURRENCY_BALLISTA': { - name: '', - description: '', + name: 'Ballista Points Cap', + description: 'Ballista Points currency cap.', }, 'MAIN.CAP_CURRENCY_SPARKS': { - name: '', - description: '', + name: 'Sparks Cap', + description: 'Sparks currency cap.', }, 'MAIN.CAP_CURRENCY_VALOR': { - name: '', - description: '', + name: 'Valor Cap', + description: 'Valor currency cap.', }, 'MAIN.CASKET_DROP_RATE': { - name: '', + name: 'Treasure Casket Drop Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue * 100}%`; + } + return '???'; + }, }, 'MAIN.CHEST_MAX_ILLUSION_TIME': { - name: '', - description: '', + name: 'Chest Max Illusion', + description: + 'Together with min time, determines the random range in which loot is unavailable from treasure chests.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.CHEST_MIN_ILLUSION_TIME': { - name: '', - description: '', - }, - 'MAIN.CHOCOBO_RAISING_DISABLE_RETIREMENT': { - name: '', - description: '', - }, - 'MAIN.CHOCOBO_RAISING_GIL_MULTIPLIER': { - name: '', - description: '', - }, - 'MAIN.CHOCOBO_RAISING_STAT_GROWTH_CAP': { - name: '', - description: '', - }, - 'MAIN.CHOCOBO_RAISING_STAT_NEG_MULTIPLIER': { - name: '', - description: '', - }, - 'MAIN.CHOCOBO_RAISING_STAT_POS_MULTIPLIER': { - name: '', - description: '', + name: 'Chest Min Illusion', + description: + 'Together with max time, determines the random range in which loot is unavailable from treasure chests.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.COFFER_MAX_ILLUSION_TIME': { - name: '', - description: '', + name: 'Coffer Max Illusion', + description: + 'Together with min time, determines the random range in which loot is unavailable from treasure coffers.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.COFFER_MIN_ILLUSION_TIME': { - name: '', - description: '', + name: 'Coffer Max Illusion', + description: + 'Together with min time, determines the random range in which loot is unavailable from treasure coffers.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.COSMO_CLEANSE_BASE_COST': { - name: '', - description: '', + name: 'Cosmo Cleanse Cost', + description: 'Base gil cost for a Cosmo Cleanse from Sagheera.', }, 'MAIN.CURE_POWER': { - name: '', - description: '', + name: 'Cure Power', + description: + 'Multiplies amount healed from Healing Magic, including the relevant Blue Magic.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.CURRENCY_EXCHANGE_RATE': { - name: '', - description: '', + name: 'Dynamis Exchange Rate', + description: 'Currency exchange rate for small to large Dynamis currency.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue}:1`; + } + return '???'; + }, }, 'MAIN.DAILY_TALLY_AMOUNT': { - name: '', - description: '', + name: 'Daily Tally', + description: 'Amount of Daily Tally points granted per day.', }, 'MAIN.DAILY_TALLY_LIMIT': { - name: '', - description: '', + name: 'Daily Tally Cap', + description: 'Daily Tally currency cap.', }, 'MAIN.DARK_POWER': { - name: '', - description: '', - }, - 'MAIN.DEBUG_CHOCOBO_RAISING': { - name: '', - description: '', + name: 'Dark Magic Power', + description: 'Multiplies amount drained by Dark Magic.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.DIA_OVERWRITE': { - name: '', - description: '', + name: 'Bio Overwrites Dia', + description: 'Bio overwrites same tier Dia.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.DIGGING_RATE': { - name: '', - description: '', - }, - 'MAIN.DIG_ABUNDANCE_BONUS': { - name: '', - description: '', + name: 'Dig Rate', + description: + 'Chance to receive an item from chocobo digging during favorable weather.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.DIG_FATIGUE': { - name: '', - description: '', + name: 'Dig Fatigue', + description: 'Fatigue system for chocobo digging.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.DIG_GRANT_BORE': { - name: '', - description: '', + name: 'Dig Grant Bore', + description: 'Grants Bore dig ability.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.DIG_GRANT_BURROW': { - name: '', - description: '', - }, - 'MAIN.DISABLE_INACTIVITY_WATCHDOG': { - name: '', - description: '', + name: 'Dig Grant Burrow', + description: 'Grants Burrow dig ability.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.DISABLE_PARTY_EXP_PENALTY': { - name: '', - description: '', + name: 'Party EXP Penalty', + description: 'EXP is not penalized by party size.', + transform: (arg) => { + return !arg; + }, }, 'MAIN.DIVINE_POWER': { - name: '', - description: '', + name: 'Divine Magic Power', + description: 'Multiplies damage dealt by Divine Magic.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.DYNA_LEVEL_MIN': { - name: '', - description: '', + name: 'Dynamis Entry', + description: 'Minimum level for entering Dynamis.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.DYNA_MIDNIGHT_RESET': { - name: '', - description: '', + name: 'Dynamis Midnight Reset', + description: + 'Dynamis lockout resets at midnight instead of hours since last entry.', }, 'MAIN.ELEMENTAL_DEBUFF_DURATION': { - name: '', - description: '', + name: 'Elemental Debuff Duration', + description: 'Base duration of elemental debuffs.', + transform: (arg) => { + return `${arg}s`; + }, }, 'MAIN.ELEMENTAL_POWER': { - name: '', - description: '', - }, - 'MAIN.ENABLE_ABYSSEA': { - name: '', - description: '', - }, - 'MAIN.ENABLE_ACP': { - name: '', - description: '', - }, - 'MAIN.ENABLE_AMK': { - name: '', - description: '', - }, - 'MAIN.ENABLE_ASA': { - name: '', - description: '', + name: 'Elemental Magic Power', + description: + 'Multiplies damage dealt by Elemental and non-drain Dark Magic.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.ENABLE_CHOCOBO_RAISING': { - name: '', - description: '', - }, - 'MAIN.ENABLE_COP': { - name: '', + name: 'Chocobo Raising', description: '', }, 'MAIN.ENABLE_COP_ZONE_CAP': { - name: '', - description: '', + name: 'Capped CoP Zones', + description: 'CoP zones use their original level cap.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_DAILY_TALLY': { - name: '', - description: '', + name: 'Daily Tally', + description: 'Allows acquisition of daily points for gobbie mystery box.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_EXCHANGE_100S_TO_1S': { - name: '', - description: '', + name: 'Can Downgrade Dynamis Currency', + description: + 'Allows exchange of 100s to 1s, like you can with 10Ks to 100s.', }, 'MAIN.ENABLE_EXCHANGE_LIMIT': { - name: '', - description: '', + name: 'Sparks Spend Limit', + description: + 'Limits amount of sparks and Unity accolades that can be spent per week.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_FIELD_MANUALS': { - name: '', + name: 'Fields of Valor', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_GARRISON': { - name: '', + name: 'Garrison', description: '', }, 'MAIN.ENABLE_GROUNDS_TOMES': { - name: '', + name: 'Grounds of Valor', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_IMMUNOBREAK': { - name: '', + name: 'Immunobreak', description: '', }, 'MAIN.ENABLE_LOGIN_CAMPAIGN': { - name: '', + name: 'Login Campaign', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_MAGIAN_TRIALS': { - name: '', + name: 'Magian Trials', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_MONSTROSITY': { - name: '', + name: 'Monstrosity', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_NYZUL_CASKETS': { - name: '', - description: '', + name: 'Nyzul Caskets', + description: 'Treasure caskets drop from NMs.', }, 'MAIN.ENABLE_ROE': { - name: '', + name: 'Records of Eminence', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_ROE_TIMED': { - name: '', - description: '', - }, - 'MAIN.ENABLE_ROV': { - name: '', - description: '', - }, - 'MAIN.ENABLE_SOA': { - name: '', - description: '', + name: 'RoE Timed Objectives', + description: '4-hour timed records.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_SURVIVAL_GUIDE': { - name: '', - description: '', - }, - 'MAIN.ENABLE_TOAU': { - name: '', + name: 'Survival Guides', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_TRUST_ALTER_EGO_EXPO': { - name: '', - description: '', - }, - 'MAIN.ENABLE_TRUST_ALTER_EGO_EXPO_ANNOUNCE': { - name: '', - description: '', + name: 'Alter Ego Expo', + description: 'HP%/MP%/Status Resistance', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_TRUST_ALTER_EGO_EXTRAVAGANZA': { - name: '', - description: '', - }, - 'MAIN.ENABLE_TRUST_ALTER_EGO_EXTRAVAGANZA_ANNOUNCE': { - name: '', - description: '', + name: 'Alter Ego Extravaganza', + description: 'Certain extra trusts are available.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_TRUST_CASTING': { name: 'Trusts', description: '', - }, - 'MAIN.ENABLE_TRUST_CUSTOM_ENGAGEMENT': { - name: '', - description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_TRUST_QUESTS': { - name: '', - description: '', - }, - 'MAIN.ENABLE_TVR': { - name: '', - description: '', + name: 'Trust Quests', + description: 'Trust unlock quests.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENABLE_VIGIL_DROPS': { - name: '', - description: '', + name: 'Nyzul Vigil Weapon', + description: 'Vigil weapons drops from NMs.', }, 'MAIN.ENABLE_VOIDWALKER': { - name: '', - description: '', - }, - 'MAIN.ENABLE_VOIDWATCH': { - name: '', - description: '', - }, - 'MAIN.ENABLE_WOTG': { - name: '', - description: '', + name: 'Voidwalker', + description: 'Voidwalker NMs are enabled.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.ENM_COOLDOWN': { - name: '', - description: '', + name: 'ENM Cooldown', + description: 'Time before a player can obtain same KI for ENMs.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue}hr`; + } + return '???'; + }, }, 'MAIN.EQUIP_FROM_OTHER_CONTAINERS': { - name: '', - description: '', + name: 'Equip From Bags', + description: + 'Allows equipping items from Mog Satchel, Sack, and Case. Only possible with the use of client addons.', }, 'MAIN.EXCAVATION_BREAK_CHANCE': { - name: '', - description: '', + name: 'Excavation Break Chance', + description: 'Chance for the pickaxe to break during excavation.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.EXCAVATION_RATE': { - name: '', - description: '', + name: 'Excavation Rate', + description: 'Chance to receive an item from excavation.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.EXPLORER_MOOGLE_LV': { - name: '', - description: '', + name: 'Explorer Moogle', + description: 'Explorer Moogle teleportation.', + transform: (arg) => { + if (arg === 0) { + return 'disabled'; + } + return `Lv.${arg}`; + }, }, 'MAIN.EXP_RATE': { - name: '', + name: 'Quest EXP Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.FORCE_SPAWN_QM_RESET_TIME': { - name: '', - description: '', + name: '??? Respawn', + description: 'Time ??? remains hidden after the mob it spawns despawns.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.FOV_REWARD_ALLIANCE': { - name: '', - description: '', + name: 'FoV Alliance', + description: + 'Allows Fields of Valor rewards while being a member of an alliance.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.FREE_COP_DYNAMIS': { - name: '', - description: '', - }, - 'MAIN.FRIGICITE_TIME': { - name: '', - description: '', + name: 'Unlocked CoP Dynamis', + description: 'Allows entry to CoP Dynamis without mission completion.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.GARRISON_LOCKOUT': { - name: '', - description: '', + name: 'Garrison Lockout', + description: 'Time before a new garrison can be started.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.GARRISON_NATION_BYPASS': { - name: '', - description: '', + name: 'Garrison Nation Bypass', + description: 'Bypasses garrison nation requirements.', }, 'MAIN.GARRISON_ONCE_PER_WEEK': { - name: '', - description: '', + name: 'Multiple Garrisons Per Week', + description: 'Allows more than one garrison per Conquest tally week.', + transform: (arg) => { + return !arg; + }, }, 'MAIN.GARRISON_PARTY_LIMIT': { - name: '', - description: '', + name: 'Garrison Party Limit', + description: 'Max party members for garrison.', }, 'MAIN.GARRISON_RANK': { - name: '', - description: '', + name: 'Garrison Nation Rank', + description: 'Minimum nation rank to start garrison.', }, 'MAIN.GARRISON_TIME_LIMIT': { - name: '', - description: '', + name: 'Garrison Time Limit', + description: 'Time before lose ongoing garrison.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.GIL_RATE': { - name: '', + name: 'Quest Gil Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.GOBBIE_BOX_MIN_AGE': { - name: '', - description: '', + name: 'Mystery Box Age', + description: + 'Minimum age before a character can sign up for Gobbie Mystery Box.', + transform: (arg) => { + return `${arg} days`; + }, }, 'MAIN.GOV_REWARD_ALLIANCE': { - name: '', - description: '', - }, - 'MAIN.HALLOWEEN_2005': { - name: '', - description: '', - }, - 'MAIN.HALLOWEEN_YEAR_ROUND': { - name: '', - description: '', + name: 'GoV Alliance', + description: + 'Allows Grounds of Valor rewards while being a member of an alliance.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.HARVESTING_BREAK_CHANCE': { - name: '', - description: '', + name: 'Harvesting Break Chance', + description: 'Chance for the sickle to break during harvesting.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.HARVESTING_RATE': { - name: '', - description: '', + name: 'Harvesting Rate', + description: 'Chance to receive an item from harvesting.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.HEALING_TP_CHANGE': { - name: '', - description: '', + name: 'Healing TP Change', + description: 'Change in TP for each healing tick. (/heal)', }, 'MAIN.HOMEPOINT_TELEPORT': { - name: '', - description: '', - }, - 'MAIN.INACTIVITY_WATCHDOG_PERIOD': { - name: '', + name: 'Home Point Teleport', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.INITIAL_LEVEL_CAP': { - name: '', + name: 'Initial Level Cap', description: '', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.ITEM_POWER': { - name: '', - description: '', + name: 'Item Power', + description: 'Effect of items such as Potions and Ethers.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.LANTERNS_STAY_LIT': { - name: '', - description: '', + name: 'Den of Rancor Lanterns', + description: 'Time that lanterns in the Den of Rancor stay lit.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'MAIN.LOGGING_BREAK_CHANCE': { - name: '', - description: '', + name: 'Logging Break Chance', + description: 'Chance for the hatchet to break during logging.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.LOGGING_RATE': { - name: '', - description: '', - }, - 'MAIN.MAX_LEVEL': { - name: '', - description: '', + name: 'Logging Rate', + description: 'Chance to receive an item from logging.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.MINING_BREAK_CHANCE': { - name: '', - description: '', + name: 'Mining Break Chance', + description: 'Chance for the pickaxe to break during mining.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.MINING_RATE': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_DONT_WIPE_BUFFS': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_INFAMY_MESSAGING': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_INFAMY_RATIO': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_PVP_MODE': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_PVP_ZONE_BYPASS': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_TELEPORT_TO_FERETORY': { - name: '', - description: '', - }, - 'MAIN.MONSTROSITY_TRIGGER_NPCS': { - name: '', - description: '', + name: 'Mining Rate', + description: 'Chance to receive an item from mining.', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAIN.NEW_CHARACTER_CUTSCENE': { - name: '', + name: 'New Character Cutscene', description: '', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.NINJUTSU_POWER': { - name: '', + name: 'Ninjutsu Power', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.NM_LOTTERY_CHANCE': { - name: '', + name: 'NM Lottery Chance', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + if (numValue < 0) { + return '100%'; + } + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.NM_LOTTERY_COOLDOWN': { - name: '', + name: 'NM Lottery Cooldown', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + if (numValue < 0) { + return '0s'; + } + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.NORMAL_MOB_MAX_LEVEL_RANGE_MAX': { - name: '', - description: '', + name: 'Mob Max Level Range Max', + description: 'Upper bound of max level range for normal mobs.', + transform: (arg) => { + return arg === 0 ? 'none' : arg; + }, }, 'MAIN.NORMAL_MOB_MAX_LEVEL_RANGE_MIN': { - name: '', - description: '', + name: 'Mob Max Level Range Min', + description: 'Lower bound of max level range for normal mobs.', + transform: (arg) => { + return arg === 0 ? 'none' : arg; + }, }, 'MAIN.NUMBER_OF_DM_EARRINGS': { - name: '', - description: '', + name: 'Divine Might Earrings', + description: + 'Number of earrings players can simultaneously own from Divine Might.', }, 'MAIN.OLDSCHOOL_G1': { - name: '', - description: '', + name: 'Old School Limit Break 1', + description: + 'Requires farming Exoray Mold, Bomb Coal, and Ancient Papyrus drops instead of allowing key item method.', }, 'MAIN.OLDSCHOOL_G2': { - name: '', - description: '', + name: 'Old School Limit Break 2', + description: + 'Requires the NMs for "Atop the Highest Mountains" be dead to get KI.', }, 'MAIN.PRISMATIC_HOURGLASS_COST': { - name: '', - description: '', + name: 'Prismatic Hourglass Cost', + description: 'Cost of the prismatic hourglass for Dynamis.', }, 'MAIN.REGIME_REWARD_THRESHOLD': { - name: '', - description: '', + name: 'Regime Reward Threshold', + description: + 'Max levels below FoV/GoV minimum suggested range player can earn EXP.', }, 'MAIN.REGIME_WAIT': { - name: '', - description: '', + name: 'Regime Cooldown', + description: 'Enables FoV/GoV game day cooldown.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.RELIC_2ND_UPGRADE_WAIT_TIME': { - name: '', - description: '', + name: 'Relic 2nd Stage Wait', + description: 'Wait time for 2nd relic upgrade.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60 / 60}hr`; + } + return '???'; + }, }, 'MAIN.RELIC_3RD_UPGRADE_WAIT_TIME': { - name: '', - description: '', - }, - 'MAIN.RESTRICT_CONTENT': { - name: '', - description: '', + name: 'Relic 3rd Stage Wait', + description: 'Wait time for 3rd relic upgrade.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60 / 60}hr`; + } + return '???'; + }, }, 'MAIN.RIVERNE_PORTERS': { - name: '', - description: '', + name: 'Riverne Teleports', + description: + 'Time that Unstable Displacements in Cape Riverne stay open after trading a scale.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue}s`; + } + return '???'; + }, }, 'MAIN.ROE_EXP_RATE': { - name: '', + name: 'RoE EXP Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.RUNIC_DISK_SAVE': { - name: '', - description: '', - }, - 'MAIN.SERVER_MESSAGE': { - default: - 'Please visit https://github.com/LandSandBoat/server for the latest information on the project.\nThank you, and we hope you enjoy sailing the sands!', - name: '', - description: '', - }, - 'MAIN.SERVER_NAME': { - name: '', - description: '', + name: 'Runic Disk Save', + description: 'Allows anyone participating in Nyzul to save progress.', }, 'MAIN.SHOP_PRICE': { - name: '', + name: 'Shop Prices', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.SNEAK_INVIS_DURATION_MULTIPLIER': { - name: '', + name: 'Sneak/Invis Duration', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.SPARKS_RATE': { - name: '', + name: 'Sparks Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.SPIKE_EFFECT_DURATION': { - name: '', - description: '', + name: 'Spikes Effect Duration', + description: 'Duration of RDM, BLM spikes effects.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue}s`; + } + return '???'; + }, }, 'MAIN.START_GIL': { - name: '', - description: '', + name: 'Starting Gil', + description: 'Amount of gil given to newly created characters.', }, 'MAIN.START_INVENTORY': { - name: '', - description: '', + name: 'Starting Inventory Size', + description: 'Starting inventory and satchel size.', }, 'MAIN.STONESKIN_CAP': { - name: '', - description: '', + name: 'Stoneskin Cap', + description: 'Soft cap for hp absorbed by stoneskin.', }, 'MAIN.SUBJOB_QUEST_LEVEL': { - name: '', - description: '', + name: 'Subjob Quest Level', + description: 'Minimum level to accept either subjob quest.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAIN.TABS_RATE': { - name: '', + name: 'Tabs Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.TIMELESS_HOURGLASS_COST': { - name: '', - description: '', - }, - 'MAIN.TRUST_ALTER_EGO_EXPO_MESSAGE': { - default: - '? ????? The Alter Ego Expo Campaign is active! ?????Trusts gain the benefits of Increased HP, MP, and Status Resistances!', - name: '', - description: '', - }, - 'MAIN.TRUST_ALTER_EGO_EXTRAVAGANZA_MESSAGE': { - default: - '? ????? The Alter Ego Extravaganza Campaign is active! ?????This is an excellent time to fill out your roster of Trusts!', - name: '', - description: '', + name: 'Timeless Hourglass Cost', + description: 'Refund for the timeless hourglass for Dynamis.', }, 'MAIN.UNLOCK_OUTPOST_WARPS': { - name: '', - description: '', + name: 'All Outposts', + description: 'New characters receive all outpost warps.', + transform: (arg) => { + return !!arg; + }, }, 'MAIN.USE_ADOULIN_WEAPON_SKILL_CHANGES': { - name: '', - description: '', + name: 'Adoulin Weapon Skill Calculations', + description: 'Uses new Adoulin weapon skill damage calculations.', }, 'MAIN.USE_OLD_CURE_FORMULA': { - name: '', - description: '', + name: 'Old Cure Formula', + description: 'Uses older cure formula.', }, 'MAIN.USE_OLD_MAGIC_DAMAGE': { - name: '', - description: '', + name: 'Old Magic Damage', + description: 'Uses older magic damage formulas.', }, 'MAIN.WEAPON_SKILL_POWER': { - name: '', + name: 'Weapon Skill Power', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAIN.WEEKLY_EXCHANGE_LIMIT': { - name: '', - description: '', + name: 'Sparks Weekly Limit', + description: + 'Maximum amount of sparks and Unity accolades that can be spent per week.', }, 'MAP.ABILITY_RECAST_MULTIPLIER': { - name: '', + name: 'Ability Recast', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.AH_BASE_FEE_SINGLE': { - name: '', - description: '', + name: 'AH Base Fee Single', + description: 'Base auction house fee for single items.', }, 'MAP.AH_BASE_FEE_STACKS': { - name: '', - description: '', + name: 'AH Base Fee Stack', + description: 'Base auction house fee for stacks.', }, 'MAP.AH_LIST_LIMIT': { - name: '', - description: '', + name: 'AH List Limit', + description: 'Max open listings per player.', + transform: (arg) => { + if (arg === 0) { + return 'unlimited'; + } + return arg; + }, }, 'MAP.AH_MAX_FEE': { - name: '', - description: '', + name: 'AH Max Fee', + description: 'Max auction house fee.', }, 'MAP.AH_TAX_RATE_SINGLE': { - name: '', + name: 'AH Tax Rate Single', description: '', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAP.AH_TAX_RATE_STACKS': { - name: '', + name: 'AH Tax Rate Stack', description: '', + transform: (arg) => { + return `${arg}%`; + }, }, 'MAP.ALL_JOBS_WIDESCAN': { - name: '', - description: '', + name: 'All Jobs Widescan', + description: 'Jobs other than BST and RNG have widescan.', }, 'MAP.ALL_MOBS_GIL_BONUS': { - name: '', - description: '', + name: 'All Mobs Bonus Gil', + description: + 'All mobs drop this much extra gil per mob level even if they normally drop zero.', }, 'MAP.ALTER_EGO_HP_MULTIPLIER': { - name: '', + name: 'Trust HP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.ALTER_EGO_MP_MULTIPLIER': { - name: '', + name: 'Trust MP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.ALTER_EGO_SKILL_MULTIPLIER': { - name: '', + name: 'Trust Skill', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.ALTER_EGO_STAT_MULTIPLIER': { - name: '', + name: 'Trust Stats', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.ANTICHEAT_ENABLED': { - name: '', - description: '', - }, - 'MAP.ANTICHEAT_JAIL_DISABLE': { - name: '', - description: '', - }, - 'MAP.AUDIT_CHAT': { - name: '', - description: '', - }, - 'MAP.AUDIT_GM_CMD': { - name: '', - description: '', + name: 'Anticheat', + description: 'Server side anti-cheating measurements active.', }, 'MAP.AUDIT_LINKSHELL': { - name: '', + name: 'Logs Linkshell Chat', description: '', }, 'MAP.AUDIT_PARTY': { - name: '', + name: 'Logs Party Chat', description: '', }, 'MAP.AUDIT_SAY': { - name: '', + name: 'Logs Say Chat', description: '', }, 'MAP.AUDIT_SHOUT': { - name: '', + name: 'Logs Shout Chat', description: '', }, 'MAP.AUDIT_TELL': { - name: '', + name: 'Logs Tells', description: '', }, 'MAP.AUDIT_UNITY': { - name: '', + name: 'Logs Unity Chat', description: '', }, 'MAP.AUDIT_YELL': { - name: '', + name: 'Logs Yells', description: '', }, 'MAP.BATTLE_CAP_TWEAK': { - name: '', - description: '', + name: 'Battlefield Level Cap', + description: + 'Globally adjusts ALL battlefield level caps by this many levels.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 0 ? '' : '+'}${numValue}`; + } + return '???'; + }, }, 'MAP.BLOCK_OLD_SKILLUP_STYLE': { - name: '', - description: '', - }, - 'MAP.BLOCK_TELL_TO_HIDDEN_GM': { - name: '', - description: '', + name: 'Old Block Skill Increase', + description: 'Allows block to skill up regardless of the action occurring.', }, 'MAP.BLOOD_PACT_SHARED_TIMER': { - name: '', - description: '', + name: 'Blood Pact Shared Timer', + description: '"Blood Pact: Rage" and "Blood Pact: Ward" share a timer.', }, 'MAP.CAPACITY_RATE': { - name: '', + name: 'Capacity Points Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.CRAFT_AMOUNT_MULTIPLIER': { - name: '', + name: 'Craft Skill Increase', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.CRAFT_CHANCE_MULTIPLIER': { - name: '', + name: 'Craft Skill Increase Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.CRAFT_COMMON_CAP': { - name: '', - description: '', + name: 'Craft Common Cap', + description: + 'Craft level limit from which specialization points beginning to count.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${(numValue / 10).toFixed()}`; + } + return '???'; + }, }, 'MAP.CRAFT_MODERN_SYSTEM': { - name: '', - description: '', + name: 'Original Crafting System', + description: 'Uses original skill up rates and margins.', + transform: (arg) => { + return !arg; + }, }, 'MAP.CRAFT_SPECIALIZATION_POINTS': { - name: '', - description: '', + name: 'Craft Specialization Points', + description: 'Amount of points allowed in crafts over the common cap.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${(numValue / 10).toFixed()}`; + } + return '???'; + }, }, 'MAP.DESPAWN_JUGPETS_BELOW_MINIMUM_LEVEL': { - name: '', - description: '', + name: 'Despawn Jug Pets When Capped', + description: + 'Despawns jug pets that have a minimum level below level sync or zone level restriction.', }, 'MAP.DISABLE_GEAR_SCALING': { - name: '', - description: '', + name: 'Gear Scaling', + description: + 'Ability to equip higher level gear when level cap/sync effect is on.', + transform: (arg) => { + return !arg; + }, }, 'MAP.DROP_RATE_MULTIPLIER': { - name: '', + name: 'Mob Drop Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.ENABLE_ITEM_RECYCLE_BIN': { - name: '', + name: 'Recycling Bin', description: '', }, 'MAP.ENMITY_CAP': { - name: '', + name: 'Enmity Cap', description: '', }, 'MAP.EXP_LOSS_LEVEL': { - name: '', - description: '', + name: 'EXP Loss', + description: 'Minimum level at which experience points can be lost.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAP.EXP_LOSS_RATE': { - name: '', + name: 'EXP Loss Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.EXP_PARTY_GAP_NO_EXP': { - name: '', - description: '', + name: 'EXP Max Party Level Gap', + description: + "A party member's experience points are nullified if the level difference with the highest-level party member exceeds this value.", }, 'MAP.EXP_PARTY_GAP_PENALTIES': { - name: '', - description: '', + name: 'EXP Party Level Penalties', + description: 'Penalizes EXP based on level differences in the party.', }, 'MAP.EXP_RATE': { - name: '', - description: '', + name: 'EXP Rate', + description: 'Does not account for EXP table changes.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.EXP_RETAIN': { - name: '', - description: '', + name: 'EXP Retained on Death', + description: 'Percentage of experience normally lost kept upon death.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `+${numValue * 100}%`; + } + return '???'; + }, }, 'MAP.FAME_MULTIPLIER': { - name: '', + name: 'Fame Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.FELLOW_TP_MULTIPLIER': { - name: '', + name: 'Fellow TP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.FISHING_ENABLE': { - name: '', + name: 'Fishing', description: '', }, 'MAP.FISHING_SKILL_MULTIPLIER': { - name: '', + name: 'Fishing Skill Increase Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.GARDEN_DAY_MATTERS': { - name: '', + name: 'Gardening Day Matters', description: '', }, 'MAP.GARDEN_MH_AURA_MATTERS': { - name: '', + name: 'Gardening Aura Matters', description: '', }, 'MAP.GARDEN_MOONPHASE_MATTERS': { - name: '', + name: 'Gardening Moon Phase Matters', description: '', }, 'MAP.GARDEN_POT_MATTERS': { - name: '', + name: 'Gardening Pot Matters', description: '', }, 'MAP.GUARD_OLD_SKILLUP_STYLE': { - name: '', - description: '', + name: 'Old Guard Skill Increase', + description: 'Allows guard to skill up regardless of the action occurring.', }, 'MAP.HEALING_TICK_DELAY': { - name: '', - description: '', + name: 'Healing Tick', + description: 'Delay between healing ticks.', + transform: (arg) => { + return `${arg}s`; + }, }, 'MAP.INCLUDE_MOB_SJ': { - name: '', - description: '', + name: 'Mob Subjob Adjust', + description: 'Mob subjobs are affected by subjob ratio.', }, 'MAP.KEEP_JUGPET_THROUGH_ZONING': { - name: '', + name: 'Keep Jug Pets Through Zone', description: '', }, 'MAP.LEVEL_SYNC_ENABLE': { name: 'Level Sync', description: '', }, - 'MAP.LIGHTLUGGAGE_BLOCK': { - name: '', - description: '', - }, 'MAP.LV_CAP_MISSION_BCNM': { - name: '', + name: 'Level Cap Mission BCNM', description: '', }, 'MAP.MAX_GIL_BONUS': { - name: '', - description: '', + name: 'Max Gil Bonus', + description: + 'Maximum total bonus gil that can be dropped. (All Mobs Bonus Gil)', }, 'MAP.MAX_MERIT_POINTS': { - name: '', - description: '', - }, - 'MAP.MAX_TIME_LASTUPDATE': { - name: '', - description: '', + name: 'Merit Points Cap', + description: 'Initial max allowed merits points players can hold.', }, 'MAP.MINIMUM_LEVEL_CONQUEST_INFUENCE_LOSS': { - name: '', - description: '', + name: 'Conquest Influence Loss', + description: + 'Minimum level at which regional influence is lost in conquest when a player dies.', + transform: (arg) => { + return `Lv.${arg}`; + }, }, 'MAP.MOB_ADDITIONAL_TIME_TO_DEAGGRO': { - name: '', - description: '', + name: 'Mob Additional Despawn Time', + description: 'Extra time before a mob despawns after deaggro.', + transform: (arg) => { + return `+${arg}s`; + }, }, 'MAP.MOB_GIL_MULTIPLIER': { - name: '', + name: 'Mob Gil Drops', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.MOB_HP_MULTIPLIER': { - name: '', + name: 'Mob HP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.MOB_MP_MULTIPLIER': { - name: '', + name: 'Mob MP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.MOB_NO_DESPAWN': { - name: '', - description: '', + name: 'Mob No Despawn', + description: 'Allows mobs to walk back home instead of despawning.', }, 'MAP.MOB_SPEED_MOD': { - name: '', - description: '', + name: 'Mob Aggro Speed', + description: + 'Modifier to apply to monster speed after aggro as a percentage of retail speed.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 0 ? '' : '+'}${((50 + numValue) / 50 - 1) * 100}%`; + } + return '???'; + }, }, 'MAP.MOB_STAT_MULTIPLIER': { - name: '', + name: 'Mob Stats', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.MOB_TP_MULTIPLIER': { - name: '', + name: 'Mob TP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.MOUNT_SPEED_MOD': { - name: '', - description: '', + name: 'Mount Speed', + description: 'Mount speed compared to retail.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 0 ? '' : '+'}${((40 + numValue) / 40 - 1) * 100}%`; + } + return '???'; + }, }, 'MAP.NM_HP_MULTIPLIER': { - name: '', + name: 'NM HP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.NM_MP_MULTIPLIER': { - name: '', + name: 'NM MP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.NM_STAT_MULTIPLIER': { - name: '', - description: '', - }, - 'MAP.PACKETGUARD_ENABLED': { - name: '', + name: 'NM Stats', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.PARRY_OLD_SKILLUP_STYLE': { - name: '', - description: '', + name: 'Old Parry Skill Increase', + description: 'Allows parry to skill up regardless of the action occurring.', }, 'MAP.PET_TP_MULTIPLIER': { - name: '', + name: 'Pet TP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.PLAYER_HP_MULTIPLIER': { - name: '', + name: 'Player HP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.PLAYER_MP_MULTIPLIER': { - name: '', + name: 'Player MP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.PLAYER_STAT_MULTIPLIER': { - name: '', + name: 'Player Stats', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.PLAYER_TP_MULTIPLIER': { - name: '', + name: 'Player TP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.PREVENT_UNENGAGED_WS': { - name: '', - description: '', - }, - 'MAP.REPORT_LUA_ERRORS_TO_PLAYER_LEVEL': { - name: '', - description: '', - }, - 'MAP.SETVAR_RETRY_MAX': { - name: '', + name: 'Prevent Unengaged Weapon Skills', description: '', }, 'MAP.SJ_MP_DIVISOR': { - name: '', - description: '', + name: 'Subjob MP', + description: 'The amount of MP a subjob provides to the main job.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${(1 / numValue) * 100}%`; + } + return '???'; + }, }, 'MAP.SKILLUP_AMOUNT_MULTIPLIER': { - name: '', - description: '', + name: 'Combat Skill Increase', + description: 'Skill increase amount.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.SKILLUP_BLOODPACT': { - name: '', - description: '', + name: 'Blood Pact Skill Increase', + description: 'Allows skill ups from blood pacts.', }, 'MAP.SKILLUP_CHANCE_MULTIPLIER': { - name: '', + name: 'Combat Skill Increase Rate', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.SPEED_MOD': { - name: 'Speed', + name: 'Player Speed', description: 'Player speed compared to retail.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 0 ? '' : '+'}${((50 + numValue) / 50 - 1) * 100}%`; + } + return '???'; + }, }, 'MAP.SUBJOB_RATIO': { - name: '', - description: '', + name: 'Subjob Ratio', + description: 'Subjob to main job ratio.', + transform: (arg) => { + let ret = '???'; + switch (arg) { + case 0: + ret = 'none'; + break; + case 1: + ret = '1:2'; + break; + case 2: + ret = '2:3'; + break; + case 3: + ret = '1:1'; + break; + default: + } + return ret; + }, }, 'MAP.TRUST_TP_MULTIPLIER': { - name: '', - description: '', - }, - 'MAP.VANADIEL_TIME_EPOCH': { - name: '', + name: 'Trust TP', description: '', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue < 1 ? '' : '+'}${((numValue - 1) * 100).toFixed()}%`; + } + return '???'; + }, }, 'MAP.WS_POINTS_BASE': { - name: '', - description: '', + name: 'Weapon Skill Point Base', + description: + 'Weapon skill point base (before skillchain) for breaking latent.', }, 'MAP.WS_POINTS_SKILLCHAIN': { - name: '', - description: '', + name: 'Weapon Skill Point Skillchain', + description: 'Weapon skill points per skillchain element.', }, 'MAP.YELL_COOLDOWN': { - name: '', - description: '', - }, - 'SEARCH.DEBUG_OUT_PACKETS': { - name: '', - description: '', + name: 'Yell Cooldown', + description: 'Minimum time between uses of yell command.', + transform: (arg) => { + return `${arg}s`; + }, }, 'SEARCH.EXPIRE_AUCTIONS': { - name: '', - description: '', + name: 'Expire Auctions', + description: 'Expires Auction House listings.', }, 'SEARCH.EXPIRE_DAYS': { - name: '', - description: '', + name: 'Auction List Duration', + description: 'Expires items older than this number of days.', + transform: (arg) => { + return `${arg} day`; + }, }, 'SEARCH.EXPIRE_INTERVAL': { - name: '', - description: '', + name: 'Auction Expire Interval', + description: 'Interval server checks for expired auctions.', + transform: (arg) => { + const numValue = Number(arg); + if (!Number.isNaN(numValue)) { + return `${numValue / 60} min`; + } + return '???'; + }, }, 'SEARCH.OMIT_NO_HISTORY': { - name: '', - description: '', + name: 'AH Omit No History', + description: + 'Items with no listing history are omitted from auction house results.', }, }; diff --git a/client/src/images/copy-image.png b/client/src/images/copy-image.png new file mode 100644 index 0000000..de425f3 Binary files /dev/null and b/client/src/images/copy-image.png differ diff --git a/client/src/index.css b/client/src/index.css index 1046594..4d580f1 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -5,4 +5,5 @@ * { -webkit-user-drag: none; /* Safari */ user-drag: none; + user-select: none; } diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 13331b0..2548447 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -45,30 +45,26 @@ export default function Home({ }, [setAlertInfo, setServers]); const filterServers = (server: ServerData): boolean => { - if ( - searchState.name.value.length > 0 && - typeof server.settings['MAIN.SERVER_NAME'] === 'string' - ) { - const serverName = server.settings['MAIN.SERVER_NAME'].toLowerCase(); + if (searchState.name.value.length > 0) { + const serverName = server.name.toLowerCase(); if (!serverName.includes(searchState.name.value.toLowerCase())) { return false; } } - if (typeof server.settings['MAIN.MAX_LEVEL'] === 'number') { - if (server.settings['MAIN.MAX_LEVEL'] < searchState.maxLevel.value[0]) { - return false; - } - if (server.settings['MAIN.MAX_LEVEL'] > searchState.maxLevel.value[1]) { - return false; - } + if (server.max_level < searchState.maxLevel.value[0]) { + return false; + } + if (server.max_level > searchState.maxLevel.value[1]) { + return false; } if ( searchState.trusts.value && - typeof server.settings['MAIN.ENABLE_TRUST_CASTING'] === 'number' + typeof server.customizations['MAIN.ENABLE_TRUST_CASTING'] === 'number' ) { - const serverTrusts = server.settings['MAIN.ENABLE_TRUST_CASTING'] === 1; + const serverTrusts = + server.customizations['MAIN.ENABLE_TRUST_CASTING'] === 1; const searchEnabled = searchState.trusts.value.includes('enabled'); const searchDisabled = searchState.trusts.value.includes('disabled'); if (serverTrusts && searchDisabled && !searchEnabled) { @@ -81,9 +77,9 @@ export default function Home({ if ( searchState.levelSync.value && - typeof server.settings['MAP.LEVEL_SYNC_ENABLE'] === 'boolean' + typeof server.customizations['MAP.LEVEL_SYNC_ENABLE'] === 'boolean' ) { - const serverLevelSync = server.settings['MAP.LEVEL_SYNC_ENABLE']; + const serverLevelSync = server.customizations['MAP.LEVEL_SYNC_ENABLE']; const searchEnabled = searchState.levelSync.value.includes('enabled'); const searchDisabled = searchState.levelSync.value.includes('disabled'); if (serverLevelSync && searchDisabled && !searchEnabled) { @@ -98,19 +94,19 @@ export default function Home({ const searchNoneEnabled = searchState.expansions.value.includes('none'); const searchRotzEnabled = searchState.expansions.value.includes('rotz'); const serverRotzEnabled = - server.settings['LOGIN.RISE_OF_ZILART'] === true; + server.customizations['LOGIN.RISE_OF_ZILART'] === true; const searchCopEnabled = searchState.expansions.value.includes('cop'); const serverCopEnabled = - server.settings['LOGIN.CHAINS_OF_PROMATHIA'] === true; + server.customizations['LOGIN.CHAINS_OF_PROMATHIA'] === true; const searchToauEnabled = searchState.expansions.value.includes('toau'); const serverToauEnabled = - server.settings['LOGIN.TREASURES_OF_AHT_URGHAN'] === true; + server.customizations['LOGIN.TREASURES_OF_AHT_URGHAN'] === true; const searchWotgEnabled = searchState.expansions.value.includes('wotg'); const serverWotgEnabled = - server.settings['LOGIN.WINGS_OF_THE_GODDESS'] === true; + server.customizations['LOGIN.WINGS_OF_THE_GODDESS'] === true; const searchSoaEnabled = searchState.expansions.value.includes('soa'); const serverSoaEnabled = - server.settings['LOGIN.SEEKERS_OF_ADOULIN'] === true; + server.customizations['LOGIN.SEEKERS_OF_ADOULIN'] === true; if ( searchNoneEnabled && (serverRotzEnabled || @@ -139,27 +135,22 @@ export default function Home({ } if (searchState.multibox.value) { - const serverMultibox = server.settings['LOGIN.LOGIN_LIMIT']; - if (typeof serverMultibox === 'number') { - if ( - searchState.multibox.value.includes('none') && - serverMultibox !== 1 - ) { - return false; - } - if ( - searchState.multibox.value.includes('unlimited') && - serverMultibox !== 0 - ) { - return false; - } - if ( - searchState.multibox.value.includes('limited') && - !searchState.multibox.value.includes('unlimited') && - serverMultibox < 2 - ) { - return false; - } + const serverMultibox = server.login_limit; + if (searchState.multibox.value.includes('none') && serverMultibox !== 1) { + return false; + } + if ( + searchState.multibox.value.includes('unlimited') && + serverMultibox !== 0 + ) { + return false; + } + if ( + searchState.multibox.value.includes('limited') && + !searchState.multibox.value.includes('unlimited') && + serverMultibox < 2 + ) { + return false; } }