nanoModalMenu — это ядро меню, которое предоставляет подключаемым плагинам единую оболочку интерфейса, общие темы, цвета, слои, заголовок, контентную область и обработку обновления UI.
Документация нужна для разработчиков, которые хотят встроить свой плагин в меню и сохранить общий стиль системы.
В самом конце документации присутствует плагин-пример с уже выполненным подключением и базовой отрисовкой модуля.
Вы можете использовать его как пример и проверку на работу с UI.
0. Поддерживаемые хуки
Код:
void OnInitializedCore()
/// Вызывается при инициализации ядра, обязательно используйте данный хук, чтобы подключиться к перезагрузке системы
void OnUnloadedCore()
/// Вызывается при выгрузке ядра
/// Используйте для очистки кэша и прочих данных
void OnModalMenuClose(BasePlayer player)
/// Вызывается когда игрок закрывает модальное меню
void OnModalMenuChangePage(BasePlayer player)
/// Вызывается когда игрок меняет вкладку в меню
Boolean OnOpenModalMenu(BasePlayer player, String pluginName)
/// Вызывается при открытии модального меню и/или страницы плагина в нём
/// При возврате false - не позволяет открыть данную вкладку
1. Основная идея интеграции
Подключаемый модуль не должен рисовать отдельное меню поверх системы. Он должен использовать готовую оболочку
nanoModalMenu:
- ядро создаёт основную панель меню;
- модуль создаёт только свой заголовок и контент;
- цвета берутся из темы ядра;
- при перезагрузке ядра модуль заново получает темы;
- при наличии nanoSettingModule модуль может использовать личную тему и настройки игрока.
Код:
nanoModalMenu
├─ основная панель меню
├─ список тем
├─ стандартная тема
├─ кнопки модулей
└─ хуки обновления
Подключаемый модуль
├─ получает темы
├─ получает стандартную тему
├─ рисует заголовок
├─ рисует контент
└─ использует цвета из выбранной темы
2. Подключение ядра
В модуле необходимо добавить ссылку на ядро:
C#:
[PluginReference] private Plugin nanoModalMenu;
Если модуль использует настройки игрока:
C#:
[PluginReference] private Plugin nanoModalMenu, nanoSettingModule;
Перед вызовом API всегда проверяйте наличие ядра:
C#:
if (nanoModalMenu == null)
{
PrintError("Не найдено ядро меню. Загрузите nanoModalMenu совместимой версии.");
return;
}
3. Основные слои интерфейса
Для правильной интеграции модуль должен использовать общие слои меню:
C#:
private const String layerMenuPanel = "nanoModalMenu_UI_Panel";
private const String moduleHeader = "nanoModalMenu_UI_Module_Header";
private const String moduleContent = "nanoModalMenu_UI_Module_Content";
| Слой | Назначение |
|---|
| layerMenuPanel | Родитель для последующих элементов [Создается nanoModalMenu] |
| moduleHeader | Область заголовка модуля. Здесь размещается название, описание и декоративные элементы. |
| moduleContent | Основная область модуля. Здесь размещаются кнопки, списки, категории, страницы и другой ваш контент. |
Как использовать:
C#:
elements.Add(new CuiElement
{
Name = moduleHeader,
Parent = layerMenuPanel,
DestroyUi = moduleHeader,
Components =
{
new CuiImageComponent { Color = theme.colorPanels },
new CuiRectTransformComponent
{
AnchorMin = "0.22 0.78",
AnchorMax = "0.98 0.96"
}
}
});
Важное правило: перед созданием нового заголовка или контента указывайте
DestroyUi.
Это удалит старый слой и не позволит UI накладываться друг на друга.
C#:
DestroyUi = moduleHeader;
DestroyUi = moduleContent;
Рекомендуемая структура:
Код:
nanoModalMenu_UI_Panel
├─ nanoModalMenu_UI_Module_Header
│ ├─ заголовок
│ ├─ описание
│ └─ декор
│
└─ nanoModalMenu_UI_Module_Content
├─ кнопки
├─ категории
├─ списки
└─ основной контент
4. API ядра
4.1 Получение списка тем
C#:
Object API_GetThemesJson()
Возвращает JSON-строку со всеми темами и их параметрами.
C#:
String json = nanoModalMenu.Call("API_GetThemesJson") as String;
Этот API обязателен для подключаемых модулей, так как через него модуль получает цвета для построения интерфейса.
4.2 Получение стандартной темы
C#:
String API_GetDefaultThemeKey()
Возвращает ключ стандартной темы из конфигурации ядра.
C#:
defaultKeyTheme = nanoModalMenu.Call<String>("API_GetDefaultThemeKey");
Используется как запасной вариант, если у игрока нет выбранной темы или отсутствует
nanoSettingModule.
4.3 Обновление меню игрока
C#:
void API_UpdateMenuTheme(BasePlayer player, String pluginName)
Перегенерирует меню игрока и возвращает его на страницу указанного модуля.
C#:
nanoModalMenu.Call("API_UpdateMenuTheme", player, Name);
Используется редко.
Обычно достаточно обновлять данные через хук OnInitializedCore.
5. Структура темы
Модуль должен иметь локальный класс темы с теми же полями, что и ядро:
C#:
private class Themes
{
public String colorPanels;
public String colorButton;
public String colorSelectedButton;
public String colorDisableButton;
public String colorActionButton;
public String colorSelectedActionButton;
public String colorTitle;
public String colorDescription;
public String colorTitleSelectedButton;
public String colorDescriptionSelectedButton;
public String colorTitleDisabledButton;
public String colorDescriptionDisabledButton;
public String colorSelectedLine;
public String colorDisableIconButton;
public String colorSelectedIconButton;
public String colorProgressBar;
public String colorProgressLine;
public String colorStatusActive;
public String colorStatusInActive;
public String colorCooldown;
}
Хранение тем в модуле:
C#:
private Dictionary<String, Themes> themeList = new Dictionary<String, Themes>();
private String defaultKeyTheme;
6. Загрузка тем из ядра
Рекомендуется загружать темы безопасно: проверять пустой JSON, ошибки парсинга, неизвестные и отсутствующие поля.
C#:
private sealed class SafeParseResult<TValue>
{
public Boolean success;
public String fatalError;
public readonly Dictionary<String, TValue> parsed = new Dictionary<String, TValue>();
public readonly List<String> warnings = new List<String>();
}
Пример загрузки:
C#:
private void LoadThemesFromCore(Boolean isRebuild = false)
{
if (nanoModalMenu == null)
{
PrintError("Не найдено ядро меню. Загрузите nanoModalMenu совместимой версии.");
return;
}
String json = nanoModalMenu.Call("API_GetThemesJson") as String;
SafeParseResult<Themes> result = TryDeserializeDictionarySafe<Themes>(json);
foreach (String warning in result.warnings)
PrintWarning(warning);
if (!result.success)
{
PrintError($"Не удалось загрузить темы. Ошибка: {result.fatalError}");
return;
}
themeList = result.parsed;
if (!isRebuild)
{
defaultKeyTheme = nanoModalMenu.Call<String>("API_GetDefaultThemeKey");
Puts($"Получен ключ стандартной темы: {defaultKeyTheme}");
}
Puts(isRebuild
? "Ядро было перезагружено, пересобираем данные о темах."
: $"Загружено тем из ядра меню: {themeList.Count}");
RebuildModuleUiCache();
}
7. Хуки загрузки и обновления
При обычной загрузке модуля:
C#:
private void OnServerInitialized()
{
LoadThemesFromCore();
}
При перезагрузке ядра:
C#:
private void OnInitializedCore() => NextTick(() =>
{
LoadThemesFromCore(true);
});
При выгрузке ядра:
C#:
private void OnUnloadedCore()
{
themeList.Clear();
cachedStaticUi.Clear();
cachedPagesUi.Clear();
}
8. Получение темы игрока
Если установлен
nanoSettingModule, можно получить личную тему игрока:
C#:
private String GetThemePlayerKey(BasePlayer player)
{
String themePlayer = defaultKeyTheme;
if (nanoSettingModule)
themePlayer = nanoSettingModule.Call<String>("API_GetSelectedThemePlayer", player);
return themePlayer;
}
Безопасное получение темы:
C#:
private Themes GetTheme(BasePlayer player)
{
String themeKey = GetThemePlayerKey(player);
if (!String.IsNullOrWhiteSpace(themeKey) && themeList.TryGetValue(themeKey, out Themes theme))
return theme;
if (!String.IsNullOrWhiteSpace(defaultKeyTheme) && themeList.TryGetValue(defaultKeyTheme, out Themes defaultTheme))
return defaultTheme;
return null;
}
9. Использование звуков интерфейса
Звук стоит вызывать только при действиях игрока: нажатие кнопки, выбор категории, переключение страницы.
C#:
private void RunEffect(BasePlayer player)
{
if (!nanoSettingModule) return;
Boolean isUseSounds = nanoSettingModule.Call<Boolean>("API_GetUseSound", player);
if (!isUseSounds) return;
EffectNetwork.Send(
new Effect(fire_select, (BaseEntity)player, 0U, new Vector3(), new Vector3()),
player.Connection
);
}
10. Заголовок модуля
Заголовок нужен, чтобы игрок сразу понимал, в каком разделе находится и что делает данный модуль.
В заголовке рекомендуется размещать:
- название модуля;
- короткое описание;
- декоративные предметы или иконки;
- цвета из текущей темы.
Пример настройки заголовка:
C#:
public class HeaderMenu
{
[JsonProperty("Мультиязычный перевод заголовка меню")]
public Dictionary<String, String> languageTitle;
[JsonProperty("Мультиязычный перевод описания меню")]
public Dictionary<String, String> languageDescription;
[JsonProperty("Список предметов для декорации меню [Shortname] (не более 9-ти штук)")]
public List<String> decorationShortnames = new List<String>();
[JsonIgnore] public List<Int32> itemsIdsIcons;
}
Пример заполнения:
C#:
headerMenu = new HeaderMenu()
{
languageTitle = new Dictionary<String, String>()
{
["ru"] = "ИНФОРМАЦИЯ",
["en"] = "INFORMATION"
},
languageDescription = new Dictionary<String, String>()
{
["ru"] = "Здесь собрана важная информация о сервере, правилах и возможностях.",
["en"] = "Here you can find important information about the server, rules and features."
},
decorationShortnames = new List<String>()
{
"spraycan",
"documents",
"tool.binoculars",
"building.planner",
"map",
"note"
}
};
11. Общий стиль модулей
Чтобы все модули выглядели частью одной системы, соблюдайте несколько правил.
Используйте цвета темы:
C#:
Color = theme.colorTitle;
Color = theme.colorDescription;
Color = theme.colorButton;
Color = theme.colorPanels;
Не рекомендуется без необходимости задавать цвета вручную:
Разделяйте заголовок и контент:
Код:
Заголовок:
├─ название
├─ описание
└─ декор
Контент:
├─ кнопки
├─ категории
├─ страницы
└─ основной интерфейс
Поддерживайте мультиязычность:
C#:
public Dictionary<String, String> languageTitle;
public Dictionary<String, String> languageDescription;
Пример получения текста:
C#:
private static String GetTextOne(String languagePlayer, ref Dictionary<String, String> languageText)
{
if (languageText.TryGetValue(languagePlayer, out String playerText))
return playerText;
if (languageText.TryGetValue("en", out String englishText))
return englishText;
return languageText.FirstOrDefault().Value ?? "NONE LANGUAGE";
}
12. Минимальный пример построения заголовка
C#:
private void DrawHeader(BasePlayer player)
{
Themes theme = GetTheme(player);
if (theme == null) return;
CuiElementContainer container = new CuiElementContainer();
container.Add(new CuiElement
{
Name = moduleHeader,
Parent = layerMenuPanel,
DestroyUi = moduleHeader,
Components =
{
new CuiImageComponent { Color = theme.colorPanels },
new CuiRectTransformComponent
{
AnchorMin = "0.22 0.78",
AnchorMax = "0.98 0.96"
}
}
});
container.Add(new CuiElement
{
Parent = moduleHeader,
Components =
{
new CuiTextComponent
{
Text = "ПРИМЕР МОДУЛЯ",
Color = theme.colorTitle,
Font = "robotocondensed-bold.ttf",
FontSize = 28,
Align = TextAnchor.UpperLeft
},
new CuiRectTransformComponent
{
AnchorMin = "0.03 0.50",
AnchorMax = "0.95 0.90"
}
}
});
container.Add(new CuiElement
{
Parent = moduleHeader,
Components =
{
new CuiTextComponent
{
Text = "Короткое описание возможностей модуля.",
Color = theme.colorDescription,
Font = "robotocondensed-regular.ttf",
FontSize = 14,
Align = TextAnchor.UpperLeft
},
new CuiRectTransformComponent
{
AnchorMin = "0.03 0.18",
AnchorMax = "0.95 0.48"
}
}
});
CuiHelper.AddUi(player, container);
}
13. Минимальный пример построения контента
C#:
private void DrawContent(BasePlayer player)
{
Themes theme = GetTheme(player);
if (theme == null) return;
CuiElementContainer container = new CuiElementContainer();
container.Add(new CuiElement
{
Name = moduleContent,
Parent = layerMenuPanel,
DestroyUi = moduleContent,
Components =
{
new CuiImageComponent { Color = "0 0 0 0" },
new CuiRectTransformComponent
{
AnchorMin = "0.22 0.08",
AnchorMax = "0.98 0.76"
}
}
});
container.Add(new CuiElement
{
Parent = moduleContent,
Components =
{
new CuiImageComponent { Color = theme.colorButton },
new CuiRectTransformComponent
{
AnchorMin = "0.02 0.82",
AnchorMax = "0.98 0.98"
}
}
});
container.Add(new CuiElement
{
Parent = moduleContent,
Components =
{
new CuiTextComponent
{
Text = "Контент вашего модуля",
Color = theme.colorTitle,
Font = "robotocondensed-bold.ttf",
FontSize = 18,
Align = TextAnchor.MiddleLeft
},
new CuiRectTransformComponent
{
AnchorMin = "0.04 0.82",
AnchorMax = "0.95 0.98"
}
}
});
CuiHelper.AddUi(player, container);
}
14. Пример добавления модуля в конфигурацию ядра
В конфигурации
nanoModalMenu добавьте кнопку модуля в список кнопок:
JSON:
"example": {
"Название плагина [Который поддерживает данное меню]": "nanoExampleModule",
"Название иконки из ..data/nanoSystem/nanoModalMenu/Images": "EXAMPLE_ICON_BUTTON",
"Мультиязычное название кнопки": {
"ru": "Пример",
"en": "Example"
},
"Мультиязычное описание кнопки": {
"ru": "Демонстрационный модуль",
"en": "Demo module"
},
"Активна ли данная кнопка": true
}
Готовый тестовый плагин :
Используйте команду
nano.test при открытом модальном меню, чтобы вызвать вкладку с тестовым интерфейсом.
Пример кода (кликабельно)