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