From ff95b3d00f1fb3c113243c79ec763bd804aa847f Mon Sep 17 00:00:00 2001 From: Kwoth Date: Thu, 30 Sep 2021 08:53:15 +0200 Subject: [PATCH] =?UTF-8?q?-=20.streamsclear=20re-added.=20It=20will=20rem?= =?UTF-8?q?ove=20all=20followed=20streams=20on=20the=20server.=20-=20.gift?= =?UTF-8?q?s=20now=20have=203=20new=20=E2=9C=82=EF=B8=8F=20Haircut=20?= =?UTF-8?q?=F0=9F=A7=BB=20ToiletPaper=20and=20=F0=9F=A5=80=20WiltedRose=20?= =?UTF-8?q?which=20**reduce**=20waifu's=20value=20-=20Added=20a=20new=20mu?= =?UTF-8?q?ltiplier=20(waifu.multi.negative=5Fgift=5Feffect=20default=200.?= =?UTF-8?q?5,=20changeable=20via=20.config=20gambling=20or=20data/gambling?= =?UTF-8?q?.yml)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 + .../Modules/Gambling/Common/GamblingConfig.cs | 16 +- .../Services/GamblingConfigService.cs | 28 +- .../Modules/Gambling/Services/WaifuService.cs | 29 +- .../Modules/Gambling/WaifuClaimCommands.cs | 8 +- .../Games/Common/Hangman/HangmanGame.cs | 1 - .../Services/StreamNotificationService.cs | 23 +- .../Searches/StreamNotificationCommands.cs | 16 +- src/NadekoBot/data/aliases.yml | 3 + src/NadekoBot/data/gambling.yml | 263 ++++++++++-------- .../data/strings/commands/commands.en-US.yml | 9 +- .../strings/responses/responses.en-US.json | 1 + 12 files changed, 247 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5087d137e..70d233861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog. ## Unreleased +### Added +- `.streamsclear` re-added. It will remove all followed streams on the server. +- `.gifts` now have 3 new โœ‚๏ธ Haircut ๐Ÿงป ToiletPaper and ๐Ÿฅ€ WiltedRose which **reduce** waifu's value + - They are called negative gifts + - They show up at the end of the `.gifts` page and are marked with a broken heart + - They have a separate multiplier (`waifu.multi.negative_gift_effect` default 0.5, changeable via `.config gambling` or `data/gambling.yml`) + - When gifted, the waifu's price will be reduced by the `price * multiplier` + - Negative gifts don't show up in `.waifuinfo` nor is the record of them kept in the database + ## [3.0.6] - 27.09.2021 ### Added diff --git a/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs b/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs index 00f380012..435a3e3d5 100644 --- a/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs +++ b/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Cloneable; using NadekoBot.Common; using NadekoBot.Common.Yml; +using YamlDotNet.Serialization; namespace NadekoBot.Modules.Gambling.Common { @@ -179,7 +180,8 @@ default is 0.02, which is 2%")] public MultipliersData Multipliers { get; set; } = new MultipliersData(); - [Comment(@"List of items available for gifting.")] + [Comment(@"List of items available for gifting. +If negative is true, gift will instead reduce waifu value.")] public List Items { get; set; } = new List(); public WaifuConfig() @@ -260,6 +262,11 @@ Default 1 (meaning no effect)")] Default 0.95 (meaning 95%) Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")] public decimal GiftEffect { get; set; } = 0.95M; + + [Comment(@"What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'. +Default 0.5 (meaning 50%) +Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)")] + public decimal NegativeGiftEffect { get; set; } = 0.50M; } [Cloneable] @@ -268,19 +275,24 @@ Example: If a waifu is worth 1000, and she receives a gift worth 100, her new va public string ItemEmoji { get; set; } public int Price { get; set; } public string Name { get; set; } + + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public bool Negative { get; set; } public WaifuItemModel() { } - public WaifuItemModel(string itemEmoji, int price, string name) + public WaifuItemModel(string itemEmoji, int price, string name, bool negative = false) { ItemEmoji = itemEmoji; Price = price; Name = name; + Negative = negative; } + public override string ToString() => Name; } diff --git a/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs b/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs index 2b1247b8f..7767d379b 100644 --- a/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs @@ -1,7 +1,11 @@ -๏ปฟusing NadekoBot.Common; +๏ปฟusing System; +using System.Collections.Generic; +using System.Linq; +using NadekoBot.Common; using NadekoBot.Common.Configs; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Services; +using NadekoBot.Services.Database.Models; namespace NadekoBot.Modules.Gambling.Services { @@ -34,9 +38,31 @@ namespace NadekoBot.Modules.Gambling.Services AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("waifu.multi.negative_gift_effect", gs => gs.Waifu.Multipliers.NegativeGiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1); AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0); AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + + Migrate(); + } + + private readonly IEnumerable antiGiftSeed = new[] + { + new WaifuItemModel("๐Ÿฅ€", 100, "WiltedRose", true), + new WaifuItemModel("โœ‚๏ธ", 1000, "Haircut", true), + new WaifuItemModel("๐Ÿงป", 10000, "ToiletPaper", true), + }; + + public void Migrate() + { + if (_data.Version < 2) + { + ModifyConfig(c => + { + c.Waifu.Items = c.Waifu.Items.Concat(antiGiftSeed).ToList(); + c.Version = 2; + }); + } } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs b/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs index acab14878..51021a959 100644 --- a/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs @@ -394,19 +394,28 @@ namespace NadekoBot.Modules.Gambling.Services }); } - w.Items.Add(new WaifuItem() + if (!itemObj.Negative) { - Name = itemObj.Name.ToLowerInvariant(), - ItemEmoji = itemObj.ItemEmoji, - }); - - if (w.Claimer?.UserId == from.Id) - { - w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect); + w.Items.Add(new WaifuItem() + { + Name = itemObj.Name.ToLowerInvariant(), + ItemEmoji = itemObj.ItemEmoji, + }); + + if (w.Claimer?.UserId == from.Id) + { + w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect); + } + else + { + w.Price += itemObj.Price / 2; + } } else { - w.Price += itemObj.Price / 2; + w.Price -= (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect); + if (w.Price < 1) + w.Price = 1; } await uow.SaveChangesAsync(); @@ -512,7 +521,7 @@ namespace NadekoBot.Modules.Gambling.Services { var conf = _gss.Data; return conf.Waifu.Items - .Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name)) + .Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name, x.Negative)) .ToList(); } } diff --git a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index eb474dbef..30be8faaf 100644 --- a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -317,10 +317,14 @@ namespace NadekoBot.Modules.Gambling .WithOkColor(); waifuItems - .OrderBy(x => x.Price) + .OrderBy(x => x.Negative) + .ThenBy(x => x.Price) .Skip(9 * cur) .Take(9) - .ForEach(x => embed.AddField($"{x.ItemEmoji} {x.Name}", x.Price, true)); + .ForEach(x => embed + .AddField($"{(!x.Negative ? string.Empty : "\\๐Ÿ’”")} {x.ItemEmoji} {x.Name}", + Format.Bold(x.Price.ToString()) + _config.Currency.Sign, + true)); return embed; }, waifuItems.Count, 9); diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs index 8fdac4c68..006ab3ec9 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs @@ -73,7 +73,6 @@ namespace NadekoBot.Modules.Games.Hangman return new(output); } - // todo lock public State Guess(string guess) { if (CurrentPhase != Phase.Running) diff --git a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs index d8043df8a..5032acabd 100644 --- a/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/Services/StreamNotificationService.cs @@ -14,6 +14,8 @@ using NadekoBot.Extensions; using StackExchange.Redis; using Discord; using Discord.WebSocket; +using LinqToDB; +using LinqToDB.EntityFrameworkCore; using NadekoBot.Common.Collections; using NadekoBot.Common.Replacements; using NadekoBot.Db; @@ -342,19 +344,18 @@ namespace NadekoBot.Modules.Searches.Services return Task.CompletedTask; } - public int ClearAllStreams(ulong guildId) + public async Task ClearAllStreams(ulong guildId) { - // todo future clear streams - int count; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams)); - count = gc.FollowedStreams.Count; - gc.FollowedStreams.Clear(); - uow.SaveChanges(); - } + using var uow = _db.GetDbContext(); + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.FollowedStreams)); + uow.RemoveRange(gc.FollowedStreams); - return count; + foreach (var s in gc.FollowedStreams) + await PublishUnfollowStream(s); + + uow.SaveChanges(); + + return gc.FollowedStreams.Count; } public async Task UnfollowStreamAsync(ulong guildId, int index) diff --git a/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs index 7a067b078..749a113b6 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotificationCommands.cs @@ -69,14 +69,14 @@ namespace NadekoBot.Modules.Searches fs.Type)); } - // [NadekoCommand, Usage, Description, Aliases] - // [RequireContext(ContextType.Guild)] - // [UserPerm(GuildPerm.Administrator)] - // public async Task StreamsClear() - // { - // var count = _service.ClearAllStreams(ctx.Guild.Id); - // await ReplyErrorLocalizedAsync(strs.streams_cleared(count))); - // } + [NadekoCommand, Usage, Description, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task StreamsClear() + { + var count = _service.ClearAllStreams(ctx.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.streams_cleared); + } [NadekoCommand, Aliases] [RequireContext(ContextType.Guild)] diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 0562682a5..ce85b4ab7 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -532,6 +532,9 @@ streamadd: streamremove: - streamremove - strm +streamsclear: + - streamsclear + - stclear streamlist: - streamlist - stl diff --git a/src/NadekoBot/data/gambling.yml b/src/NadekoBot/data/gambling.yml index f184f0ad1..636e31592 100644 --- a/src/NadekoBot/data/gambling.yml +++ b/src/NadekoBot/data/gambling.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 1 +version: 2 # Currency settings currency: # What is the emoji/character which represents the currency @@ -21,12 +21,12 @@ betRoll: # This setting will describe which multiplier is used for when the roll is higher than the given number. # Doesn't have to be ordered. pairs: - - whenAbove: 99 - multiplyBy: 10 - - whenAbove: 90 - multiplyBy: 4 - - whenAbove: 66 - multiplyBy: 2 + - whenAbove: 99 + multiplyBy: 10 + - whenAbove: 90 + multiplyBy: 4 + - whenAbove: 66 + multiplyBy: 2 # Automatic currency generation settings. generation: # when currency is generated, should it also have a random password @@ -68,14 +68,14 @@ decay: wheelOfFortune: # Self-Explanatory. Has to have 8 values, otherwise the command won't work. multipliers: - - 1.7 - - 1.5 - - 0.2 - - 0.1 - - 0.3 - - 0.5 - - 1.2 - - 2.4 + - 1.7 + - 1.5 + - 0.2 + - 0.1 + - 0.3 + - 0.5 + - 1.2 + - 2.4 # Settings related to waifus waifu: # Minimum price a waifu can have @@ -107,116 +107,133 @@ waifu: # Default 0.95 (meaning 95%) # Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095) giftEffect: 0.95 + # What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'. +# Default 0.5 (meaning 50%) +# Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950) + negativeGiftEffect: 0.50 # List of items available for gifting. +# If negative is true, gift will instead reduce waifu value. items: - - itemEmoji: "๐Ÿฅ”" - price: 5 - name: Potato - - itemEmoji: "๐Ÿช" - price: 10 - name: Cookie - - itemEmoji: "๐Ÿฅ–" - price: 20 - name: Bread - - itemEmoji: "๐Ÿญ" - price: 30 - name: Lollipop - - itemEmoji: "๐ŸŒน" - price: 50 - name: Rose - - itemEmoji: "๐Ÿบ" - price: 70 - name: Beer - - itemEmoji: "๐ŸŒฎ" - price: 85 - name: Taco - - itemEmoji: "๐Ÿ’Œ" - price: 100 - name: LoveLetter - - itemEmoji: "๐Ÿฅ›" - price: 125 - name: Milk - - itemEmoji: "๐Ÿ•" - price: 150 - name: Pizza - - itemEmoji: "๐Ÿซ" - price: 200 - name: Chocolate - - itemEmoji: "๐Ÿฆ" - price: 250 - name: Icecream - - itemEmoji: "๐Ÿฃ" - price: 300 - name: Sushi - - itemEmoji: "๐Ÿš" - price: 400 - name: Rice - - itemEmoji: "๐Ÿ‰" - price: 500 - name: Watermelon - - itemEmoji: "๐Ÿฑ" - price: 600 - name: Bento - - itemEmoji: "๐ŸŽŸ" - price: 800 - name: MovieTicket - - itemEmoji: "๐Ÿฐ" - price: 1000 - name: Cake - - itemEmoji: "๐Ÿ“”" - price: 1500 - name: Book - - itemEmoji: "๐Ÿฑ" - price: 2000 - name: Cat - - itemEmoji: "๐Ÿถ" - price: 2001 - name: Dog - - itemEmoji: "๐Ÿผ" - price: 2500 - name: Panda - - itemEmoji: "๐Ÿ’„" - price: 3000 - name: Lipstick - - itemEmoji: "๐Ÿ‘›" - price: 3500 - name: Purse - - itemEmoji: "๐Ÿ“ฑ" - price: 4000 - name: iPhone - - itemEmoji: "๐Ÿ‘—" - price: 4500 - name: Dress - - itemEmoji: "๐Ÿ’ป" - price: 5000 - name: Laptop - - itemEmoji: "๐ŸŽป" - price: 7500 - name: Violin - - itemEmoji: "๐ŸŽน" - price: 8000 - name: Piano - - itemEmoji: "๐Ÿš—" - price: 9000 - name: Car - - itemEmoji: "๐Ÿ’" - price: 10000 - name: Ring - - itemEmoji: "๐Ÿ›ณ" - price: 12000 - name: Ship - - itemEmoji: "๐Ÿ " - price: 15000 - name: House - - itemEmoji: "๐Ÿš" - price: 20000 - name: Helicopter - - itemEmoji: "๐Ÿš€" - price: 30000 - name: Spaceship - - itemEmoji: "๐ŸŒ•" - price: 50000 - name: Moon + - itemEmoji: "๐Ÿฅ”" + price: 5 + name: Potato + - itemEmoji: "๐Ÿช" + price: 10 + name: Cookie + - itemEmoji: "๐Ÿฅ–" + price: 20 + name: Bread + - itemEmoji: "๐Ÿญ" + price: 30 + name: Lollipop + - itemEmoji: "๐ŸŒน" + price: 50 + name: Rose + - itemEmoji: "๐Ÿบ" + price: 70 + name: Beer + - itemEmoji: "๐ŸŒฎ" + price: 85 + name: Taco + - itemEmoji: "๐Ÿ’Œ" + price: 100 + name: LoveLetter + - itemEmoji: "๐Ÿฅ›" + price: 125 + name: Milk + - itemEmoji: "๐Ÿ•" + price: 150 + name: Pizza + - itemEmoji: "๐Ÿซ" + price: 200 + name: Chocolate + - itemEmoji: "๐Ÿฆ" + price: 250 + name: Icecream + - itemEmoji: "๐Ÿฃ" + price: 300 + name: Sushi + - itemEmoji: "๐Ÿš" + price: 400 + name: Rice + - itemEmoji: "๐Ÿ‰" + price: 500 + name: Watermelon + - itemEmoji: "๐Ÿฑ" + price: 600 + name: Bento + - itemEmoji: "๐ŸŽŸ" + price: 800 + name: MovieTicket + - itemEmoji: "๐Ÿฐ" + price: 1000 + name: Cake + - itemEmoji: "๐Ÿ“”" + price: 1500 + name: Book + - itemEmoji: "๐Ÿฑ" + price: 2000 + name: Cat + - itemEmoji: "๐Ÿถ" + price: 2001 + name: Dog + - itemEmoji: "๐Ÿผ" + price: 2500 + name: Panda + - itemEmoji: "๐Ÿ’„" + price: 3000 + name: Lipstick + - itemEmoji: "๐Ÿ‘›" + price: 3500 + name: Purse + - itemEmoji: "๐Ÿ“ฑ" + price: 4000 + name: iPhone + - itemEmoji: "๐Ÿ‘—" + price: 4500 + name: Dress + - itemEmoji: "๐Ÿ’ป" + price: 5000 + name: Laptop + - itemEmoji: "๐ŸŽป" + price: 7500 + name: Violin + - itemEmoji: "๐ŸŽน" + price: 8000 + name: Piano + - itemEmoji: "๐Ÿš—" + price: 9000 + name: Car + - itemEmoji: "๐Ÿ’" + price: 10000 + name: Ring + - itemEmoji: "๐Ÿ›ณ" + price: 12000 + name: Ship + - itemEmoji: "๐Ÿ " + price: 15000 + name: House + - itemEmoji: "๐Ÿš" + price: 20000 + name: Helicopter + - itemEmoji: "๐Ÿš€" + price: 30000 + name: Spaceship + - itemEmoji: "๐ŸŒ•" + price: 50000 + name: Moon + - itemEmoji: "๐Ÿฅ€" + price: 100 + name: WiltedRose + negative: true + - itemEmoji: โœ‚๏ธ + price: 1000 + name: Haircut + negative: true + - itemEmoji: "๐Ÿงป" + price: 10000 + name: ToiletPaper + negative: true # Amount of currency selfhosters will get PER pledged dollar CENT. # 1 = 100 currency per $. Used almost exclusively on public nadeko. patreonCurrencyPerCent: 1 diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index e749655ad..1fe4a9484 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -902,6 +902,10 @@ streamadd: desc: "Notifies this channel when the stream on the specified URL goes online or offline. Offline notifications will only show if you enable `{0}streamoff`. Maximum 10 per server." args: - "twitch.tv/someguy" +streamsclear: + desc: "Removes all followed streams on this server." + args: + - "" streamremove: desc: "Stops following the stream on the specified index. (use `{0}stl` to see indexes)" args: @@ -1433,7 +1437,10 @@ waifutransfer: args: - "@ExWaifu @NewOwner" waifugift: - desc: "Gift an item to someone. This will increase their waifu value by 50% of the gifted item's value if they are not your waifu, or 95% if they are. Provide no parameters to see a list of items that you can gift." + desc: -| + Gift an item to someone. This will increase their waifu value by a percentage of the gift's value. + Negative gifts will not show up in waifuinfo. + Provide no parameters to see a list of items that you can gift. args: - "" - "Rose @Himesama" diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 8e139f3aa..9f63e1b88 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -507,6 +507,7 @@ "streamer_online": "Streamer {0} is online with {1} viewers.", "streams_follow_title": "Streams you're following", "streams_none": "You are not following any streams on this server.", + "streams_cleared": "All streams followed on this server have been removed.", "stream_no": "No such stream.", "stream_off_enabled": "Stream notifications will now show when a stream goes offline.", "stream_off_disabled": "Stream notifications will no longer show when a stream goes offline.",