Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][CR]Home Base #22311

Closed
wants to merge 22 commits into from

Conversation

Projects
None yet
5 participants
@Theundyingcode
Copy link
Contributor

commented Oct 31, 2017

Here is the long overdue PR to open my addition of bases up to review. Most of it is currently either partially implemented or just declarations especially since I have been frequently changing things including large mechanics change that just scrapped a lot of code the other day. However, it should be well documented and filled out enough for people to understand where I'm going. Also, I wrote a first draft of the help page so code reviewers: start there.

Essentially, the idea is construct a "command core" inside a building and that building gets designated as your base. The command core can be anything from a desk you write notes on to a super fancy computer console. A better core gives you more functionality and allows you to create a "command system" by adding "auxiliary systems." Remote systems like speakers and defenses can then be linked to the command system through the use of a network module. Network and CPU capacity are then added to prevent someone running NORAD from a desk with a laptop on it.

The end goal is to make an environment where NPCs can effectively live and be useful in the player's absence. For now I have said if someone is going to agree to live here then at minimum they will demand a place to sleep and stash there stuff resulting in a loose population limit.

I'm adding another layer to the map similar to traps and effects for flags specific to the base which isn't that useful at the moment but will be essential come time to add NPC base behavior.

NOTES:

  • This is my first time working in C++. A lot of the code is written by copying what other files are do. I started from scratch rather than using the base camp file because I wanted the learning experience.

  • I was under the assumption that submap = map tile which is apparently not the case. My implementation basically involves loading the base's map into the base object so I'm going to need some help here. Getting #22300 merged in would probably help.

  • Think adding additional json field for network and cpu consumption would be best. That way it won't be hard coded.

-I have yet to even look at the json parser for saving/loading the base.

@Theundyingcode Theundyingcode changed the title [WIP]{CR]Home Base [WIP][CR]Home Base Oct 31, 2017

and pasted out on their bed. By writting on a piece of furnature, it and all contained items can be personally
claimed or designated for communal use. NPCs will consume food, reload and, rearm themselves from communal storages \
and will deposit unwanted stuff there. Doors can also be marked \"No Access!\" to prevent unwanted intrusion.") );
}

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

This is tolerable for testing, but it will be mandatory to push it to json before it's mergeable.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

??? is there a help.json? I literally just opened up help.cpp and copied how everything else was done.

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

help.cpp is badly written, old file. I guess adding this block to it will not make it any worse...
But do keep it for later. A help file shouldn't describe features that don't exist yet.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

For now I''l just delete the menu reference and leave the block as basically a giant comment.


//data members
std::unordered_map<ammotype_id, int> gun_count; /**stores <ammo_type, #guns_that_use_them>.*/
std::unordered_map<ammotype_id, int> ammo_count; /**stores <ammo_types, amount>.*/

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

This is much harder to keep updated than you think. Leave it for much later.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

won't actually be useful until based npcs are actually added so sure.

/**
* Stores capacities of command system and the auxiliary systems present.
*/
struct command_sys

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

Leave this whole structure for the very end. Don't define things like processing power until you have all the mundane stuff done.

desktop.addentry( 4, true, MENU_AUTOASSIGN, _("View Status.") );
desktop.addentry( 5, true, MENU_AUTOASSIGN, _("Base Planner.") )
if (base.get_level() > 2){//add lv.3 options
desktop.addentry( 6, true, MENU_AUTOASSIGN, _("Personel Managment.") );

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

All of those things are perfectly mundane. They shouldn't need a computer. Only things like machinery settings and automatic security should require computers.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

I'll change them. The levels were kinda arbitrarily assigned anyways.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

I feel that there should be some recognition that while it's possible to do most things with pen/paper a lot of stuff just won't be worth the effort for a survivor without the help of a computer; You can make detailed construction plans on paper but it takes orders of magnitude more time than with CAD software. Maybe saying basic stuff like designating areas (crafting, sleeping etc./ define area to guard) would be for pen/paper but construction plans require software?

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

Leave limiting for much later. First get it to work completely with no limits on player's options, then break it up into levels in further PR.

* TODO: Finish writting. Change to uimenu.
*/
void iexamine::base_control( player &p, const tripoint &examp) {
#define base (p->home)

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

This looks like a bad approach. The player should not "own" a base, rather the base should "be owned" by the player.
You can save owner's ID in base structure - player has a getID (I might have misspelled it) function.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

And please don't use macros like this. You can create a "local alias" via references:

auto &base = p->home;

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

Since it's already getting passed a reference to the player it seemed easier to just add a reference to the base in player. I'll definitely add an "owner" reference in base. Still feel player is going to want to have a reference to the base though... short term as long as you can access the base object from iexamine.cpp, a "has a base" boolean will suffice.

int x = coreloc.x;
int y = coreloc.y;

//TODO: Re-write. Previous implementation wasn't going to work. (immpossible to distinguish between underground and thick rock wall)

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

Scanning area isn't easy, but start_location.cpp has an implementation of flood fill that would work to find all the reachable area from current point.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

I'm about 90% sure one of my old homework assignments for AI class will work with only a few minor tweaks... and converting it from python. I haven't even opened start_location.cpp yet but I'll take a look before implementing something.

[ [ "scrap", 2 ] ]
]
},{
"result" : "notice_screen",

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

Adding recipes that are used in only one place leads to bloating of the crafting screen. Would be better if you just included the requirements in the construction.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

Good point. Will change.

"flags" : [ "BASE_CMD2", "BASE_CORE", "CMD_SYS", "FLAMMABLE" ]
},{
"type:" : "furniture",
"id" : "f_base_cmd3",

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Oct 31, 2017

Contributor

Merge the first 3 levels - there is no need to keep them separate, especially not before getting the whole thing to work.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

I have basic levels of function in mind otherwise it's just a difference of power. Using pen and paper, having a computer and having a fancy console. Access to a computer should cause a pretty big increase in functionality. The console was going to be a requirement for most of the aux systems... I'll cut it down to 3 levels and decide priorities from there.

base::define_base_area(base_map, coreloc);
base_map.spawns.clear();//remove spawns so nothing apears in base just outside it.
base_level = 1;
food_ration_lv = ammo_ration_lv = med_ration_lv = 0; //no rationing to start

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

Members should be initialized in the member initialization list, not assigned in the body of the constructor.

*/
bool base_home::has_base_flag(const tripoint &p, base_area_flag flag)
{
return(baflag[p.x][p.y])

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

The function ignores the flag parameter. Why?

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

I wasn't thinking straight?

@@ -0,0 +1,142 @@
#pragma once
#ifndef BASE_HOME
#define BASE_HOME

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

Please add a "_H" suffix to the define as it's done in all the other headers.

void change_lv(int new_level);
void run_design();

inline int get_level()

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

inline is redundant here. Functions defined inside a class definition are automatically inline.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

didn't know that. ty

p.home.set_med_ration(lv-1);
}
case 4: {
goto top_menu;

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

Please don't use goto, ever. It considered to be one of the most awful features of the language. Seriously, google "c++ goto" and forget that it exists.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

Yeah, I figured that was hacky. I decided it should loop as an afterthought. Pretty sure that's the first time I have used goto since programing my calculator instead of paying attention in high school.

}
inline int get_max_pop()
{
return max(bunks.length, storage_open.length) + npcs.length;

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

Is this supposed to call std::max? If so, you have to add the std:: prefix.

Also note: when you want to call functions, you must use brackets. bunks.length is not a function call. Yes, even if there are no parameters, the () are still required.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

function of this being first use of c++. That's how you do it in java.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

When I learned Java (several versions ago) it required brackets for function calls as well. Accessing properties (called data members in C++) are done without them, but bunks is a std::list and length is a member function of it, not a data member.

Btw (I missed this previously): getter functions and generally all functions that don't change the object should be const, as in int get_max() const { ... }.

@@ -629,6 +629,8 @@ player::player() : Character()

morale.reset( new player_morale() );
last_craft.reset( new craft_command() );

home = NULL;

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

player::home is an object of type base_home, not a pointer. You can't assign NULL to it.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

ment for player::home to be a way to reference base not the actual object.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 1, 2017

Author Contributor

changed base::home to a pointer

* Mannage basic and security remote systems. (Lights! Cammera! Action...of connected turrets!)
*/
void iexamine::base_sec( player &p, const tripoint &examp ) {
add_msg("Not implemented yet.","(╮°-°)╮┳━━┳ ( ╯°□°)╯ ┻━━┻");

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

What are those additional arguments supposed to be? They are not used at all (there is no "%s" in the message).

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

It;s someone flipping a table in frustration. It's just supposed to be another line and it won't be staying anyways.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

It's just supposed to be another line

If you want another message line, either call add_msg again (each call generates one line), or add a line break to the message: add_msg("Line 1\nLine 2").

And another thing: string literals that should be translated must be surrounded by _( and ) - see examples all over the code.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 1, 2017

Author Contributor

I noticed the _( ) thing and started putting it in without knowing the meaning. The line is just a placeholder until I actually write the method since an empty method looks weird to me.

@@ -1033,6 +1034,38 @@ A: Ask the helpful people on the forum at smf.cataclysmdda.com or at the irc cha
return text;
}

std::vector<std::string> text_bases()

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

This function is never called.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 1, 2017

Author Contributor

That's fine (see coothulu's help.cpp review)

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 1, 2017

Contributor

It means you should scrap the function, though.
You can recover it from git history later, don't keep it commented out.

@@ -62,6 +62,7 @@ Press q or ESC to return to the game." ) ) + 1;
headers.push_back( _( "n: Unarmed Styles" ) );
headers.push_back( _( "o: Survival Tips" ) );
headers.push_back( _( "p: Driving" ) );
headers.push_back( _( "q: Bases" ) );

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

The key 'q' is already used to leave the menu.

class base_home
{

base(submap &base_map, const tripoint &coreloc);

This comment has been minimized.

Copy link
@BevapDin

BevapDin Oct 31, 2017

Contributor

Is this supposed to be the constructor? If so, it must have the same name as the class itself, in this case base_home.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Oct 31, 2017

Author Contributor

Oops I changed the class name at some point.

@BevapDin

This comment has been minimized.

Copy link
Contributor

commented Oct 31, 2017

What Coolthulhu said. Don't try to implement everything at once. Make a simple implementation (no level, no caching, as basic as it gets). Than extend it with other PRs.

@Theundyingcode

This comment has been minimized.

Copy link
Contributor Author

commented Oct 31, 2017

Don't try to implement everything at once.

Since I am essentially trying to add a whole new aspect to the game I was more concerned with getting my plans across properly. I'll trim out a bunch of stuff before it's rdy; in my experience, adding stubs and a lot of comments is much more effective than me trying to explain my thoughts.

Theundyingcode added some commits Oct 31, 2017

Address Comments
Cut to two base levels (1 and 3).
Change player::home to pointer.
Fix up iexamine::base_control.
Add ownerID to home_base.
msc changes

Theundyingcode added some commits Nov 1, 2017

Done for today
Start on define_base_area
Cut more "implement later code"
Add base map overlay
Random other changes
if (ter[temp.x][temp.y].has_flag("INDOORS") && !pushed.count(temp) ){
q.push(temp);
pushed.push(temp);
}

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 2, 2017

Contributor

Rewrite this as a set of offsets and a simple loop:

static const std::array<point, 8> offsets = {{
    { 1, 0 }, { -1, 0 }, ... etc.
}};
for( const point &off : offsets ) {
    tripoint cur_point = center + off;
    // stuff
}

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

knew that wasn't right but was blanking for some reason so copied and pasted.

auto &frn = bmap.fur;
auto &bf = bmap.bflag[x][y];
auto &in = base_flag::BASE_IN;
auto &wall = base_flag

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 2, 2017

Contributor

Don't use auto everywhere. It's useful when you have a giant structure, like std::map<tripoint, std::string>::iterator, but when it's just an int, it's much clearer when you just state it outright.

struct tripoint;
class ammunition_type;

//Furnature accepted as a storage place.

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 2, 2017

Contributor

Spellchecking: it's "furniture" not "furnature".

}

/**
* Contain's entire base map. Point is to merge base's submaps into one object.

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 2, 2017

Contributor

Operating directly on submaps by a class that isn't map is a huge red flag that you're doing something wrong.
This will cause a lot of trouble and you really don't want to do that.

I'm not sure how you want to process the camps, but it would be much safer to do this:

  • Remember the camps (all of them in given area) in overmap (not submap)
  • In a camp, note its tripoint origin - this would be the submap coordinate of its upper-left corner
  • When you need to process the camp, load a tinymap centered on camp's origin

For tinymap example, check mission_start::place_dog. For a more complex example, you can check place_caravan_ambush, but DON'T replicate that - placing tons of things that way is bad.

By noting the camps in overmap, you may be able to make them act even when the player isn't around.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

I don't think I have actually gotten my head around how all the map stuff actually works. My future plans basically have two requirements with the map: being able to add an additional set of flags to tiles in its area (npc keep out on doors, ownership to furniture etc); which I have done by adding bflag, another layer to the map grid. And, being able to to keep track of locations and get what's there (communal storage). As an extra I also want to disable the spawn points within the base because having hulks spawn inside your base is just dumb.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

Should I change base_home::overlay to be a tinymap with the extra bflag overlay? gen_overlay would then be the function call to create the tinymap.

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 3, 2017

Contributor

No
tinymap is for when the base does something. In your base structure, you only want to keep locations, not copy of the entire map.
For example, if you want to simulate a month of NPCs eating rations, you do this:

  • Calculate NPC needs for this month
  • Create a tinymap in base's origin
  • Using tinymap, go through food items in the base and delete those that the NPCs "ate"
  • Call save function of the tinymap
  • Do NOT save the tinymap structure anywhere, it should be deleted after you finish this simulation

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

Cool, thanks

@@ -1564,6 +1564,8 @@ class player : public Character, public JsonSerializer, public JsonDeserializer
*/
void set_targeting_data( const targeting_data &td );

/**Pointer to player's home base*/
base_home* home;

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 2, 2017

Contributor

That's a bad way of doing this.
Instead, you want to specify ID of the owner in base's structure.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

There already is an owner ID in base... the point of the pointer (just wanted to say that) was that iexamine functions are passed a reference to player so referencing the base becomes really easy this way. The player is going to at least want a boolean "has_base" to prevent creating a slowdown opening the construction menu since they should be limited to one home base. (future plans for smaller outposts)

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

I.e. so I can do this: auto &base = *p.home in iexamine.cpp

This comment has been minimized.

Copy link
@Coolthulhu

Coolthulhu Nov 3, 2017

Contributor

The player is going to at least want a boolean "has_base" to prevent creating a slowdown opening the construction menu since they should be limited to one home base.

The problem is, you have no way of properly setting it. The code in iexamine.cpp will only be processed when examining, so not after save load.
In order to find player's base after loading the game, you'd have to scan all overmaps in existence to find it. And overmaps may actually disappear in some cases, so simply saving a bool would be buggy.

If you want to refer to exactly one base, you should save its origin. If the base doesn't exist, you want it set to tripoint_min and comment that tripoint_min means there is no base. That way you can later (for example, when saving or loading the game) check if that base really exists.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Nov 3, 2017

Contributor

If the base doesn't exist,

You should use a cata::optional<tripoint> instead of some arbitrary magic value that requires documentation writing, reading and remembering


//Simple implementation for now
std::list<std::string> basicInfo;
basicInfo.push_back( string_format( _("%s's Home Sweet Base%n"), p.get_name(), int len );

This comment has been minimized.

Copy link
@BevapDin

BevapDin Nov 3, 2017

Contributor

int len?

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

I was going to use it to create the same underline that the other sections had. %n stores the number of characters written.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Nov 3, 2017

Contributor

Sure, but I was referring to the invalid syntax. And you don't need this. You can just examine the formatted string itself. And you don't actually care about the numbers of characters, you care about the display width (see utf8_width). There are for example Unicode combination marks that take up no additional space on the console.

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

thanks, Learned something new.

rationInfo.push_back( _("Supply Rationing") );
rationInfo.push_back( _("~~~~~~~~~~~~~~~~") );
rationInfo.push_back( string_format( _("Food: %s"), base_home::ration_levels::base.get_food_ration() );
rationInfo.push_back( string_format( _("Water: %s"), base_home::ration_levels::base.get_food_ration() );

This comment has been minimized.

Copy link
@BevapDin

BevapDin Nov 3, 2017

Contributor

What do you want to achieve with base_home::ration_levels::? Does base.get_food_ration() not work?

Also: all 4 lines print the same number (call the same function).

This comment has been minimized.

Copy link
@Theundyingcode

Theundyingcode Nov 3, 2017

Author Contributor

ration levels is an enum... get food ration levels is an int... i'll look up enum syntax and make this better

auto &peeps = p->home.get_personnel();
resInfo.push_back( _("Base Residents") );
resInfo.push_back( _("~~~~~~~~~~~~~~") );
basicInfo.push_back( _("") );//add blank line.

This comment has been minimized.

Copy link
@BevapDin

BevapDin Nov 3, 2017

Contributor

I forgot to mention this: empty strings should not be translated at all. Just use "" without the _().

And you can put this into one string, not a list (why a list and not a vector?) - and why output:: - there is no namespace output.

basicInfo.push_back( string_format( _("Communal Storage Locations: %d"), base.num_comm_storage() ) );
std::list<std::string> rationInfo;
rationInfo.push_back( _("Supply Rationing") );
rationInfo.push_back( _("~~~~~~~~~~~~~~~~") );

This comment has been minimized.

Copy link
@BevapDin

BevapDin Nov 3, 2017

Contributor

Btw this should be generated by a function. And it should not be translated (how are translators supposed to know what this means / the context).

@vache vache referenced this pull request Nov 6, 2017

Open

Settlement Ideas #24

Theundyingcode added some commits Nov 13, 2017

Add base to game, start marking, msic
Adds bases to "game" and find it through owner ID.
Adds prompt to mark base when writting/spraying.
Some work to define_base_area
@illi-kun

This comment has been minimized.

Copy link
Member

commented Feb 23, 2018

This PR is hard to review due to its size, and also it seems to be stalled and obsoleted. Feel free to re-open or, as even better option, to restart it as a bunch of much smaller PRs.

@illi-kun illi-kun closed this Feb 23, 2018

@Theundyingcode

This comment has been minimized.

Copy link
Contributor Author

commented Feb 27, 2018

yeah I haven't been involved with cdda since I got back to school... my git notifications is a scary 800+

@TheEventHorizon

This comment has been minimized.

Copy link

commented Apr 4, 2018

Any chances of this being revived?

@Theundyingcode

This comment has been minimized.

Copy link
Contributor Author

commented Apr 11, 2018

I keep meaning to come back to it but at the same time I haven't opened cdda since going back to school in January and probably won't get around to it until late summer...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.