Add minimal store UI functionality

This commit is contained in:
NGnius (Graham) 2024-01-27 15:05:41 -05:00
parent 2986c05170
commit 622f161560
13 changed files with 297 additions and 44 deletions

View file

@ -28,6 +28,7 @@ pub enum ApiMessage {
LoadSystemSettings,
GetLimits(Callback<super::SettingsLimits>),
GetProvider(String, Callback<crate::persist::DriverJson>),
UploadCurrentVariant(String, String), // SteamID, Steam username
}
pub enum BatteryMessage {
@ -251,7 +252,7 @@ impl GeneralMessage {
cb(Vec::with_capacity(0))
},
},
Self::ApplyNow => {}
Self::ApplyNow => {},
}
dirty
}
@ -304,7 +305,7 @@ impl ApiMessageHandler {
if is_persistent {
let settings_clone = settings.json();
let save_json: SettingsJson = settings_clone.into();
if let Err(e) = crate::persist::FileJson::update_variant_or_create(&save_path, save_json, settings.general.get_name().to_owned()) {
if let Err(e) = crate::persist::FileJson::update_variant_or_create(&save_path, settings.general.get_app_id(), save_json, settings.general.get_name().to_owned()) {
log::error!("Failed to create/update settings file {}: {}", save_path.display(), e);
}
//unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings");
@ -383,7 +384,7 @@ impl ApiMessageHandler {
}
ApiMessage::LoadSettings(id, name, variant_id, variant_name) => {
let path = format!("{}.ron", id);
match settings.load_file(path.into(), name, variant_id, variant_name, false) {
match settings.load_file(path.into(), id, name, variant_id, variant_name, false) {
Ok(success) => log::info!("Loaded settings file? {}", success),
Err(e) => log::warn!("Load file err: {}", e),
}
@ -391,7 +392,8 @@ impl ApiMessageHandler {
}
ApiMessage::LoadVariant(variant_id, variant_name) => {
let path = settings.general.get_path();
match settings.load_file(path.into(), settings.general.get_name().to_owned(), variant_id, variant_name, false) {
let app_id = settings.general.get_app_id();
match settings.load_file(path.into(), app_id, settings.general.get_name().to_owned(), variant_id, variant_name, false) {
Ok(success) => log::info!("Loaded settings file? {}", success),
Err(e) => log::warn!("Load file err: {}", e),
}
@ -400,6 +402,7 @@ impl ApiMessageHandler {
ApiMessage::LoadMainSettings => {
match settings.load_file(
crate::consts::DEFAULT_SETTINGS_FILE.into(),
0,
crate::consts::DEFAULT_SETTINGS_NAME.to_owned(),
0,
crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(),
@ -431,7 +434,13 @@ impl ApiMessageHandler {
_ => settings.general.provider(),
});
false
}
},
ApiMessage::UploadCurrentVariant(steam_id, steam_username) => {
//TODO
let steam_app_id = settings.general.get_app_id();
super::web::upload_settings(steam_app_id, steam_id, steam_username, settings.json());
false
},
}
}

View file

@ -87,17 +87,90 @@ fn web_config_to_settings_json(meta: community_settings_core::v1::Metadata) -> c
}
}
fn download_config(id: u128) -> std::io::Result<community_settings_core::v1::Metadata> {
let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id);
let response = ureq::get(&req_url).call()
.map_err(|e| {
log::warn!("GET to {} failed: {}", req_url, e);
std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e)
})?;
response.into_json()
}
pub fn upload_settings(id: u64, user_id: String, username: String, settings: crate::persist::SettingsJson) {
log::info!("Uploading settings {} by {} ({})", settings.name, username, user_id);
let user_id: u64 = match user_id.parse() {
Ok(id) => id,
Err(e) => {
log::error!("Failed to parse `{}` as u64: {} (aborted upload_settings very early)", user_id, e);
return;
}
};
let meta = settings_to_web_config(id as _, user_id, username, settings);
if let Err(e) = upload_config(meta) {
log::error!("Failed to upload settings: {}", e);
}
}
fn settings_to_web_config(app_id: u32, user_id: u64, username: String, settings: crate::persist::SettingsJson) -> community_settings_core::v1::Metadata {
community_settings_core::v1::Metadata {
name: settings.name,
steam_app_id: app_id,
steam_user_id: user_id,
steam_username: username,
tags: vec!["wip".to_owned()],
id: "".to_owned(),
config: community_settings_core::v1::Config {
cpus: settings.cpus.into_iter().map(|cpu| community_settings_core::v1::Cpu {
online: cpu.online,
clock_limits: cpu.clock_limits.map(|lim| community_settings_core::v1::MinMax {
min: lim.min,
max: lim.max,
}),
governor: cpu.governor,
}).collect(),
gpu: community_settings_core::v1::Gpu {
fast_ppt: settings.gpu.fast_ppt,
slow_ppt: settings.gpu.slow_ppt,
tdp: settings.gpu.tdp,
tdp_boost: settings.gpu.tdp_boost,
clock_limits: settings.gpu.clock_limits.map(|lim| community_settings_core::v1::MinMax {
min: lim.min,
max: lim.max,
}),
slow_memory: settings.gpu.slow_memory,
},
battery: community_settings_core::v1::Battery {
charge_rate: settings.battery.charge_rate,
charge_mode: settings.battery.charge_mode,
events: settings.battery.events.into_iter().map(|batt_ev| community_settings_core::v1::BatteryEvent {
trigger: batt_ev.trigger,
charge_rate: batt_ev.charge_rate,
charge_mode: batt_ev.charge_mode,
}).collect(),
},
},
}
}
fn upload_config(config: community_settings_core::v1::Metadata) -> std::io::Result<()> {
let req_url = format!("{}/api/setting", BASE_URL);
ureq::post(&req_url)
.send_json(&config)
.map_err(|e| {
log::warn!("POST to {} failed: {}", req_url, e);
std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e)
})
.map(|_| ())
}
/// Download config web method
pub fn download_new_config(sender: Sender<ApiMessage>) -> impl AsyncCallable {
let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety
let getter = move || {
let sender2 = sender.clone();
move |id: u128| {
let req_url = format!("{}/api/setting/by_id/{}", BASE_URL, id);
match ureq::get(&req_url).call() {
Ok(response) => {
let json_res: std::io::Result<community_settings_core::v1::Metadata> = response.into_json();
match json_res {
match download_config(id) {
Ok(meta) => {
let (tx, rx) = mpsc::channel();
let callback =
@ -108,14 +181,11 @@ pub fn download_new_config(sender: Sender<ApiMessage>) -> impl AsyncCallable {
.send(ApiMessage::General(GeneralMessage::AddVariant(web_config_to_settings_json(meta), Box::new(callback))))
.expect("download_new_config send failed");
return rx.recv().expect("download_new_config callback recv failed");
}
},
Err(e) => {
log::error!("Cannot parse response from `{}`: {}", req_url, e)
log::error!("Invalid response from download: {}", e);
}
}
}
Err(e) => log::warn!("Cannot get setting result from `{}`: {}", req_url, e),
}
vec![]
}
};
@ -140,3 +210,36 @@ pub fn download_new_config(sender: Sender<ApiMessage>) -> impl AsyncCallable {
},
}
}
/// Upload currently-loaded variant
pub fn upload_current_variant(sender: Sender<ApiMessage>) -> impl AsyncCallable {
let sender = Arc::new(Mutex::new(sender)); // Sender is not Sync; this is required for safety
let getter = move || {
let sender2 = sender.clone();
move |(steam_id, steam_username): (String, String)| {
sender2
.lock()
.unwrap()
.send(ApiMessage::UploadCurrentVariant(steam_id, steam_username))
.expect("upload_current_variant send failed");
true
}
};
super::async_utils::AsyncIsh {
trans_setter: |params| {
if let Some(Primitive::String(steam_id)) = params.get(0) {
if let Some(Primitive::String(steam_username)) = params.get(1) {
Ok((steam_id.to_owned(), steam_username.to_owned()))
} else {
Err("upload_current_variant missing/invalid parameter 1".to_owned())
}
} else {
Err("upload_current_variant missing/invalid parameter 0".to_owned())
}
},
set_get: getter,
trans_getter: |result| {
vec![result.into()]
},
}
}

View file

@ -77,15 +77,17 @@ fn main() -> Result<(), ()> {
let mut loaded_settings =
persist::FileJson::open(utility::settings_dir().join(DEFAULT_SETTINGS_FILE))
.map(|mut file| file.variants.remove(&0)
.map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into()))
.map(|settings| settings::Settings::from_json(DEFAULT_SETTINGS_NAME.into(), settings, DEFAULT_SETTINGS_FILE.into(), 0))
.unwrap_or_else(|| settings::Settings::system_default(
DEFAULT_SETTINGS_FILE.into(),
0,
DEFAULT_SETTINGS_NAME.into(),
0,
DEFAULT_SETTINGS_VARIANT_NAME.into())))
.unwrap_or_else(|_| {
settings::Settings::system_default(
DEFAULT_SETTINGS_FILE.into(),
0,
DEFAULT_SETTINGS_NAME.into(),
0,
DEFAULT_SETTINGS_VARIANT_NAME.into(),
@ -320,6 +322,10 @@ fn main() -> Result<(), ()> {
.register_async(
"WEB_download_new",
api::web::download_new_config(api_sender.clone())
)
.register_async(
"WEB_upload_new",
api::web::upload_current_variant(api_sender.clone())
);
utility::ioperm_power_ec();

View file

@ -9,6 +9,7 @@ use super::SettingsJson;
pub struct FileJson {
pub version: u64,
pub name: String,
pub app_id: u64,
pub variants: HashMap<u64, SettingsJson>,
}
@ -44,7 +45,7 @@ impl FileJson {
.unwrap_or(0)
}
pub fn update_variant_or_create<P: AsRef<std::path::Path>>(path: P, mut setting: SettingsJson, given_name: String) -> Result<Self, SerdeError> {
pub fn update_variant_or_create<P: AsRef<std::path::Path>>(path: P, app_id: u64, mut setting: SettingsJson, given_name: String) -> Result<Self, SerdeError> {
if !setting.persistent {
return Self::open(path)
}
@ -62,6 +63,7 @@ impl FileJson {
setting_variants.insert(setting.variant, setting);
Self {
version: 0,
app_id: app_id,
name: given_name,
variants: setting_variants,
}

View file

@ -62,6 +62,7 @@ pub fn auto_detect_provider() -> DriverJson {
let provider = auto_detect0(
None,
crate::utility::settings_dir().join("autodetect.json"),
0,
"".to_owned(),
0,
crate::consts::DEFAULT_SETTINGS_VARIANT_NAME.to_owned(),
@ -76,6 +77,7 @@ pub fn auto_detect_provider() -> DriverJson {
pub fn auto_detect0(
settings_opt: Option<&SettingsJson>,
json_path: std::path::PathBuf,
app_id: u64,
name: String,
variant_id: u64,
variant_name: String,
@ -83,6 +85,7 @@ pub fn auto_detect0(
let mut general_driver = Box::new(General {
persistent: false,
path: json_path,
app_id,
name,
variant_id,
variant_name,

View file

@ -13,14 +13,15 @@ impl Driver {
name: String,
settings: &SettingsJson,
json_path: std::path::PathBuf,
app_id: u64,
) -> Self {
let name_bup = settings.name.clone();
let id_bup = settings.variant;
auto_detect0(Some(settings), json_path, name, id_bup, name_bup)
auto_detect0(Some(settings), json_path, app_id, name, id_bup, name_bup)
}
pub fn system_default(json_path: std::path::PathBuf, name: String, variant_id: u64, variant_name: String) -> Self {
auto_detect0(None, json_path, name, variant_id, variant_name)
pub fn system_default(json_path: std::path::PathBuf, app_id: u64, name: String, variant_id: u64, variant_name: String) -> Self {
auto_detect0(None, json_path, app_id, name, variant_id, variant_name)
}
}

View file

@ -32,6 +32,7 @@ impl std::fmt::Display for SettingVariant {
pub struct General {
pub persistent: bool,
pub path: PathBuf,
pub app_id: u64,
pub name: String,
pub variant_id: u64,
pub variant_name: String,
@ -73,6 +74,14 @@ impl TGeneral for General {
self.path = path;
}
fn app_id(&mut self) -> &'_ mut u64 {
&mut self.app_id
}
fn get_app_id(&self) -> u64 {
self.app_id
}
fn get_name(&self) -> &'_ str {
&self.name
}
@ -108,7 +117,7 @@ impl TGeneral for General {
fn add_variant(&self, variant: crate::persist::SettingsJson) -> Result<Vec<crate::api::VariantInfo>, SettingError> {
let variant_name = variant.name.clone();
crate::persist::FileJson::update_variant_or_create(self.get_path(), variant, variant_name)
crate::persist::FileJson::update_variant_or_create(self.get_path(), self.get_app_id(), variant, variant_name)
.map_err(|e| SettingError {
msg: format!("failed to add variant: {}", e),
setting: SettingVariant::General,
@ -173,8 +182,8 @@ impl OnSet for Settings {
impl Settings {
#[inline]
pub fn from_json(name: String, other: SettingsJson, json_path: PathBuf) -> Self {
let x = super::Driver::init(name, &other, json_path.clone());
pub fn from_json(name: String, other: SettingsJson, json_path: PathBuf, app_id: u64) -> Self {
let x = super::Driver::init(name, &other, json_path.clone(), app_id);
log::info!(
"Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}",
x.general.provider(),
@ -190,8 +199,8 @@ impl Settings {
}
}
pub fn system_default(json_path: PathBuf, name: String, variant_id: u64, variant_name: String) -> Self {
let driver = super::Driver::system_default(json_path, name, variant_id, variant_name);
pub fn system_default(json_path: PathBuf, app_id: u64, name: String, variant_id: u64, variant_name: String) -> Self {
let driver = super::Driver::system_default(json_path, app_id, name, variant_id, variant_name);
Self {
general: driver.general,
cpus: driver.cpus,
@ -201,7 +210,7 @@ impl Settings {
}
pub fn load_system_default(&mut self, name: String, variant_id: u64, variant_name: String) {
let driver = super::Driver::system_default(self.general.get_path().to_owned(), name, variant_id, variant_name);
let driver = super::Driver::system_default(self.general.get_path().to_owned(), self.general.get_app_id(), name, variant_id, variant_name);
self.cpus = driver.cpus;
self.gpu = driver.gpu;
self.battery = driver.battery;
@ -222,6 +231,7 @@ impl Settings {
pub fn load_file(
&mut self,
filename: PathBuf,
app_id: u64,
name: String,
variant: u64,
variant_name: String,
@ -231,7 +241,7 @@ impl Settings {
if json_path.exists() {
if variant == u64::MAX {
*self.general.persistent() = true;
let file_json = FileJson::update_variant_or_create(&json_path, self.json(), variant_name.clone()).map_err(|e| SettingError {
let file_json = FileJson::update_variant_or_create(&json_path, app_id, self.json(), variant_name.clone()).map_err(|e| SettingError {
msg: format!("Failed to open settings {}: {}", json_path.display(), e),
setting: SettingVariant::General,
})?;
@ -252,7 +262,7 @@ impl Settings {
*self.general.persistent() = false;
self.general.name(name);
} else {
let x = super::Driver::init(name, settings_json, json_path.clone());
let x = super::Driver::init(name, settings_json, json_path.clone(), app_id);
log::info!("Loaded settings with drivers general:{:?},cpus:{:?},gpu:{:?},battery:{:?}", x.general.provider(), x.cpus.provider(), x.gpu.provider(), x.battery.provider());
self.general = x.general;
self.cpus = x.cpus;
@ -270,6 +280,7 @@ impl Settings {
}
*self.general.persistent() = false;
}
*self.general.app_id() = app_id;
self.general.path(filename);
self.general.variant_id(variant);
Ok(*self.general.persistent())

View file

@ -105,6 +105,10 @@ pub trait TGeneral: OnSet + OnResume + OnPowerEvent + Debug + Send {
fn path(&mut self, path: std::path::PathBuf);
fn app_id(&mut self) -> &'_ mut u64;
fn get_app_id(&self) -> u64;
fn get_name(&self) -> &'_ str;
fn name(&mut self, name: String);

View file

@ -383,6 +383,10 @@ export async function storeDownloadById(id: string): Promise<VariantInfo[]> {
return (await call_backend("WEB_download_new", [id]));
}
export async function storeUpload(steam_id: string, steam_username: string): Promise<VariantInfo[]> {
return (await call_backend("WEB_upload_new", [steam_id, steam_username]));
}
export async function getAllSettingVariants(): Promise<VariantInfo[]> {
console.log("GENERAL_get_all_variants");
return (await call_backend("GENERAL_get_all_variants", []));

View file

@ -35,6 +35,13 @@ export const CURRENT_VARIANT_GEN = "GENERAL_current_variant";
export const MESSAGE_LIST = "MESSAGE_messages";
export const INTERNAL_STEAM_ID = "INTERNAL_steam_id";
export const INTERNAL_STEAM_USERNAME = "INTERNAL_stream_username";
export const STORE_RESULTS = "INTERNAL_store_results";
export const PERIODICAL_BACKEND_PERIOD = 5000; // milliseconds
export const AUTOMATIC_REAPPLY_WAIT = 2000; // milliseconds
export const STORE_RESULTS_URI = "/plugins/PowerTools/settings_store";

View file

@ -15,6 +15,7 @@ import {
Field,
Dropdown,
SingleDropdownOption,
Navigation,
//NotchLabel
//gamepadDialogClasses,
//joinClassNames,
@ -22,6 +23,7 @@ import {
import { VFC, useState } from "react";
import { GiDrill, GiTimeBomb, GiTimeTrap, GiDynamite } from "react-icons/gi";
import { HiRefresh, HiTrash, HiPlus, HiUpload } from "react-icons/hi";
import { TbWorldPlus } from "react-icons/tb";
//import * as python from "./python";
import * as backend from "./backend";
@ -63,6 +65,12 @@ import {
MESSAGE_LIST,
INTERNAL_STEAM_ID,
INTERNAL_STEAM_USERNAME,
STORE_RESULTS,
STORE_RESULTS_URI,
PERIODICAL_BACKEND_PERIOD,
AUTOMATIC_REAPPLY_WAIT,
} from "./consts";
@ -73,10 +81,13 @@ import { Battery } from "./components/battery";
import { Cpus } from "./components/cpus";
import { DevMessages } from "./components/message";
import { StoreResultsPage } from "./store/page";
var periodicHook: NodeJS.Timeout | null = null;
var lifetimeHook: any = null;
var startHook: any = null;
var endHook: any = null;
var userHook: any = null;
var usdplReady = false;
var tryNotifyProfileChange = function() {};
@ -118,6 +129,10 @@ const reload = function() {
console.debug("POWERTOOLS: got limits ", limits);
});
if (!get_value(STORE_RESULTS)) {
backend.resolve(backend.searchStoreByAppId(0), (results) => set_value(STORE_RESULTS, results));
}
backend.resolve(backend.getBatteryCurrent(), (rate: number) => { set_value(CURRENT_BATT, rate) });
backend.resolve_nullable(backend.getBatteryChargeRate(), (rate: number | null) => { set_value(CHARGE_RATE_BATT, rate) });
backend.resolve_nullable(backend.getBatteryChargeMode(), (mode: string | null) => { set_value(CHARGE_MODE_BATT, mode) });
@ -175,6 +190,7 @@ const clearHooks = function() {
lifetimeHook?.unregister();
startHook?.unregister();
endHook?.unregister();
userHook?.unregister();
backend.log(backend.LogLevel.Info, "Unregistered PowerTools callbacks, so long and thanks for all the fish.");
};
@ -209,7 +225,6 @@ const registerCallbacks = function(autoclear: boolean) {
let appId = gameInfo.appid.toString();
backend.log(backend.LogLevel.Info, "RegisterForGameActionStart callback(" + actionType + ", " + id + ")");
// don't use gameInfo.appid, haha
backend.resolve(
backend.loadGeneralSettings(appId, gameInfo.display_name, "0", undefined),
(ok: boolean) => {
@ -221,6 +236,12 @@ const registerCallbacks = function(autoclear: boolean) {
});
}
);
backend.resolve(
backend.searchStoreByAppId(appId),
(results: backend.StoreMetadata[]) => {
set_value(STORE_RESULTS, results);
}
);
});
// this fires immediately, so let's ignore that callback
@ -236,6 +257,20 @@ const registerCallbacks = function(autoclear: boolean) {
setTimeout(() => backend.forceApplySettings(), AUTOMATIC_REAPPLY_WAIT);
});
//@ts-ignore
userHook = SteamClient.User.RegisterForCurrentUserChanges((data) => {
const accountName = data.strAccountName;
const steamId = data.strSteamID;
SteamClient.User.GetLoginUsers().then((users: any) => {
users.forEach((user: any) => {
if (user && user.accountName == accountName) {
set_value(INTERNAL_STEAM_ID, steamId);
set_value(INTERNAL_STEAM_USERNAME, user.personaName ? user.personaName : accountName);
}
});
});
});
backend.log(backend.LogLevel.Debug, "Registered PowerTools callbacks, hello!");
};
@ -373,7 +408,7 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
}}>
<DialogButton
style={{
maxWidth: "45%",
maxWidth: "30%",
minWidth: "auto",
}}
//layout="below"
@ -396,16 +431,35 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
</DialogButton>
<DialogButton
style={{
maxWidth: "45%",
maxWidth: "30%",
minWidth: "auto",
}}
//layout="below"
onClick={(_: MouseEvent) => {
backend.log(backend.LogLevel.Debug, "Clicked on unimplemented upload button");
const steamId = get_value(INTERNAL_STEAM_ID);
const steamName = get_value(INTERNAL_STEAM_USERNAME);
if (steamId && steamName) {
backend.storeUpload(steamId, steamName);
} else {
backend.log(backend.LogLevel.Warn, "Cannot upload with null steamID (is null: " + !steamId + ") and/or username (is null: " + !steamName + ")");
}
}}
>
<HiUpload/>
</DialogButton>
<DialogButton
style={{
maxWidth: "30%",
minWidth: "auto",
}}
//layout="below"
onClick={(_: MouseEvent) => {
Navigation.Navigate(STORE_RESULTS_URI);
Navigation.CloseSideMenus();
}}
>
<TbWorldPlus />
</DialogButton>
</PanelSectionRow>
<Debug idc={idc}/>
@ -453,6 +507,7 @@ export default definePlugin((serverApi: ServerAPI) => {
ico = <span><GiDynamite /><GiTimeTrap /><GiTimeBomb /></span>;
}
//registerCallbacks(false);
serverApi.routerHook.addRoute(STORE_RESULTS_URI, StoreResultsPage);
return {
title: <div className={staticClasses.Title}>PowerTools</div>,
content: <Content serverAPI={serverApi} />,

48
src/store/page.tsx Normal file
View file

@ -0,0 +1,48 @@
import { Component, Fragment } from "react";
import * as backend from "../backend";
import { tr } from "usdpl-front";
import { get_value} from "usdpl-front";
import {
STORE_RESULTS,
} from "../consts";
export class StoreResultsPage extends Component {
constructor() {
super({});
this.state = {
reloadThingy: "/shrug",
};
}
render() {
const storeItems = get_value(STORE_RESULTS) as backend.StoreMetadata[] | undefined;
console.log("POWERTOOLS: Rendering store results", storeItems);
if (storeItems) {
if (storeItems.length == 0) {
backend.log(backend.LogLevel.Warn, "No store results; got array with length 0 from cache");
return (<div>
{ tr("No results") /* TODO translate */ }
</div>);
} else {
// TODO
return storeItems.map((meta: backend.StoreMetadata) => {
<div>
<div> { meta.name } </div>
<div> { tr("Created by") /* TODO translate */} { meta.steam_username } </div>
<div> { meta.tags.map((tag: string) => <span>{tag}</span>) } </div>
Hey NG you should finish this page
</div>
});
}
} else {
backend.log(backend.LogLevel.Warn, "Store failed to load; got null from cache");
// store did not pre-load when the game started
return (<Fragment>
{ tr("Store failed to load") /* TODO translate */ }
</Fragment>);
}
}
}