[Linked!] Apartment complex for amateur (or experienced) builders looking for a place to attach their space to

At least for me, having a place that’s mine in a MUCK or MUCK-like server such as this is some what a necessity, especially if a roleplay evolves to something a little less than appropriate for public areas. For me, and others I’ve met on other platforms, that means building out a house or apartment space to invite friends to. The problem with this is that these spaces aren’t always connected to the world proper and are just hanging out in the void of unconnected rooms.

This is where an apartment complex comes into play. I’ve built (for myself and my alts) an apartment complex to link private spaces together with a public lobby, hallway, poolside space, etc. so that there’s more continuity in my personal roleplaying abode.

My suggestion would be to connect my apartment complex, currently called the “Cinnabar Prism Apartments” to the main world and allow potential residents to request an apartment from me (or potentially other complex managers). This could be anything from just providing a place for people to hook their exits to for their personal abodes, or I’d even be willing to set up a basic starter room for them with a basic description and pre-configured area and exits (and possible image, if I ever acquire a basic one).

Right now the apartment complex is an area, with each ‘apartment’ a child area set to private, with a single room containing an exit to the parent area. The child area would be transferred to the apartment owner, who is then free to build off of the initial room as they please.

Is this an idea others are interested in? Should a place like this be connected to somewhere publicly accessible from Sinder proper? (Possibly one of the vacancies connected to Sinder Crossing) If anyone has any suggestions, or would like to be included in this project before it goes public, please comment and discuss.

I, for one, think it sounds like a great idea!

We have something similar with the forest clearing, where @Raeth allows diggers to connect their dens. But we’ve been missing an apartment complex, and I welcome it :grin: (Too many live in the hostel anyway).

And if possible, I think it surely should be connected to Sinder. The map/area idea works best if rooms are somehow connected to the main world, even if the areas are private. Sinder Crossing is a good suggestion, close to the center of the realm.

Additional ideas:

  • Maybe using hidden exits to private apartments? We might hopefully end up with lots of exits. Also, we don’t have lockable exits yet.
  • Maybe also have a receptionist puppet, that can meet & greet?
  • Allow Builders (similar to Admins) also act as managers to install new apartment dwellers, when you are not around. If they control the receptionist, they can even give them new apartments fully RP style.

I am curious what others think of your idea.

/Accipiter

Awesome.
Yes, hidden exits would probably be best for now, especially while there isn’t locks available. Even if a stranger can’t do anything to your home while you’re gone, it’d still be weird for them to just walk in :joy:. Though my one concern with hidden exits as they’re currently implement is that, to my knowledge, even if they are known to the player, there is no way for them to be listed as an exit. I would love to be able to ‘learn’ a hidden exit—either after using it or with some sort of learn command—and have it appear on my exit list from then on.
After I had made the initial post I was totally thinking of a receptionist puppet, especially with the prospect of bots to automate it, but I also like the idea of having the receptionist be a puppet for builders/admins to use one for a more RP-centric experience. Possibly even have both available, a bot that takes over if no one is puppeteering the receptionist, that can automate the experience in a simple game-like way, but will back off if someone wants to puppet it. (As a software engineer I get crazy automation ideas, it could also just be a puppet)

Decided to post this here instead of cluttering the general bot request thread with my specific use case, @Accipiter. Especially since this is pertaining to opening up Cinnabar Prism for public use.

What I’m looking to make with the bot API is an attendant to create/attach rooms for future tenants. This bot would wait to be summoned with a bot command, something like

{person} says, at bot “I would like to lease an apartment.”

the bot would then check it’s database (I’d probably just use a json file containing character names or id’s and the apartment number, considering it’d just be key-value pairs) and if the person doesn’t have an apartment already reply like,

bot says, at {person} “Sure thing, let me get that ready for you.”

Or if the person already has an apartment it would reply something like,

bot says, at {person} “I’m sorry {person}, you already have an apartment with us. If you need more space try building off of your existing room(s).”

It would then take the name of the person requesting the apartment, determine which apartment number is next, then do the following:

go out
go up
create exit Apartment {unit number}
set exit Apartment {unit number}:keywords={unit number},{person}
set exit Apartment {unit number}:hidden=yes
set exit Apartment {unit number}:leaveMsg=goes inside apartment {unit number}.
set exit Apartment {unit number}:arriveMsg=comes out of apartment {unit number}.
set exit Apartment {unit number}:travelMsg=goes inside apartment {unit number}.
go {unit number}
set room name=Apartment {unit number}
set room desc={some default description, I already have one ready}
set exit Back:name=To Hallway
set exit To Hallway:keywords=exit, out, hall, hallway
set exit To Hallway:leaveMsg=leaves the apartment.
set exit To Hallway:arriveMsg=arrives from apartment 1{letter}.
set exit To Hallway:travelMsg=leaves the apartment.
create area Apartment {unit number}
set room area=Apartment {unit number}
set area Apartment {unit number}:parent=Cinnabar Prism Apartments
set area Apartment {unit number}:private=yes
request area Apartment {unit number}:owner={person}
request room owner={person}
home //This would take the bot back to where it started

It would then tell the requester,

bot says, to person "Alright, you’re all set up with your new apartment. Here are your keys, you’re in unit {unit number} ((You can get there by going out, up, then to apartment {unit number} (or alternatively, simply {unit number} or {person}))) Thank you for choosing Cinnabar Prism Apartments, we hope you enjoy your stay. Feel free to have a look around the facilities.

I think the bot should also be able to a couple more things, for instance:

  • Take an existing room supplied by a person and attach it as an apartment (this one may get tricky with areas, but we can figure that one out).
  • Greet entering characters with instructions on how to make requests
  • Provide a ‘help’ whisper repeated and/or expanding on instructions.
  • Provide other tips about the complex, like rules and facilities.
  • Accept or provide instruction on suggesting new facilities (either by mail or by mailing a real manager/builder such as myself)

Looking at what’s in the bot, it would need to be able to make predefined go, create, set, and request calls. From what I’ve gathered, these types of predefined calls aren’t there yet, as I saw that right now ActionGo is only to a random exit and the other commands are not implemented yet. I think I could make a PR to change ActionGo to have a parameter called destinationIndex or something similar to override the random index if it’s set to >= 0 (or >= 1 if it’s 1-indexed), but as of right now, I’m stumped on the others.

I LOVE IT! And all the other suggestions too :grin:
As soon as I have the time (it might be tomorrow), I will try to explain/show how all of this can be done.

This was fun to try out!
Below is a code example of a single module that does most of what you want.

To make this work, you should do the follow:

  • Pull down the latest from master branch of https://github.com/anisus/mucklet-bot
  • Copy and paste the config file example below to a new file, e.g.:
    mucklet-bot/devcfg/config-chippy.js
    
  • Edit the config file with the user & pass of your character.
  • If you don’t wish to try on https://mucklet.com , change the config URL’s to Wolfery.com’s.
  • Edit the config file and replace the REPLACE_WITH_BOT_ID text with the bot’s char ID.
    To get the bot’s ID, login to Wolfery, wakeup the bot, open the browser’s Developer Tools console, and type:
    console.log(app.getModule('player').getActiveChar().id);
    
  • Copy and paste the module example below into a new file, e.g.:
    mucklet-bot/modules/botController/reactions/reactionApartmentRequest/ReactionApartmentRequest.js
    
  • Make sure the bot is in the right room, and that its home location is correctly set.
  • Start the bot:
    node index.js devcfg/config-chippy.js
    

Now, the code can surely be made nicer, but it will give you a good idea how these things can be done. Later on, one can make better error handling, add actual help functionality, handling of messages, directly teleporting home when starting, etc.

And I have only made placeholder dummy functions to check if a character already has an apartment, and which apartment number is next in line.

But I must let you have some fun too :grin:

/Accipiter

config-chippy.js

const config = {
	api: {
		hostUrl: "wss://test.mucklet.com",
		webResourcePath: "https://test.mucklet.com/api/",
		origin: "https://mucklet.com",
	},
	login: {
		user: 'botmaster',
		pass: 'ZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNA=', // "mysecret"
	},
	botController: {
		includeChars: [ 'REPLACE_WITH_BOT_ID' ] // Replace with bot's ID
	},
	personality: {
		typeSpeed: 8000,  // 800 characters per minute
		readSpeed: 50000, // 5000 characters per minute
	},
	actionWakeup: {
		probability: 50,
	},
	reactionArriveWelcome: {
		populationChance: {
			1: 1,   // 100% chance to welcome arriving characters
		},
		priority: 150,
		delay: 1 * 1000,
		phrases: [
			"turns to {name}, \"Welcome.\". ((To get help, address me and say \"Help\".))",
		]
	},
	reactionWhisperReply: {
		chance: 1,  // 100% chance of replying to whispers
		priority: 100,
		delay: 1 * 1000,
		phrases: [
			":does not understand whispers. ((To get help, address me and say \"Help\".))",
		]
	},
};

export default config;

ReactionApartmentRequest.js

import replaceTags from '#utils/replaceTags.js';
import findById from '#utils/findById.js';

// Sleep helper function
function sleep(ms) {
	return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * ReactionApartmentRequest reacts to an apartment request.
 */
class ReactionApartmentRequest {

	/**
	 * Creates a new ReactionApartmentRequest instance.
	 * @param {App} app Modapp App object.
	 * @param {ReactionApartmentRequest~Params} params Module parameters.
	 */
	constructor(app, params) {
		this.app = app;

		this.app.require([ 'botController', 'charEvents', 'actionAddress' ], this._init);
	}

	_init = (module) => {
		this.module = Object.assign({ self: this }, module);

		this.module.botController.addAction({
			id: 'createApartment',
			exec: this._exec,
		});

		// Subscribes to events
		this.module.charEvents.subscribe(this._onCharEvent);
	}

	_onCharEvent = (char, ev) => {
		// Bot only cares about messages addressed to self.
		if (ev.type != 'address' || ev.target.id != char.id) {
			return;
		}

		// Make it a little bit "smart". Try to detect talk about leasing apartments.
		if (!ev.msg.match(/\b(lease|rent|available|free)( an)? +apartments?\b/)
			&& !ev.msg.match(/\bapartments? *(available\b|to rent\b|to lease\b|free)\b/)
		) {
			this.module.actionAddress.enqueue(
				char.id,
				ev.char.id,
				replaceTags("looks confused. ((Type `address {name} = I would like to lease an apartment.`))", ev.char),
				true,
				100
			);
			return;
		}

		// Check if we already have an apartment
		if (this._alreadyHasApartment(ev.char.id)) {
			this.module.actionAddress.enqueue(
				char.id,
				ev.char.id,
				replaceTags("I'm sorry {name}, you already have an apartment with us. If you need more space try building off of your existing room.", ev.char),
				false,
				100
			);
			return
		}

		// We could just call the API directly with the steps. But by letting
		// botController perform them as an action, we can be sure the bot only
		// creates one apartment at a time.
		this.module.botController.enqueue('createApartment', {
			charId: char.id,
			target: ev.char,
			unitNr: String(this._getNextApartmentNumber()),
			delay: 1000,
			postdelay: 2000,
			priority: 20
		});
	}

	_exec = async (player, state, outcome) => {
		let char = findById(player.controlled, outcome.charId);
		// Assert we haven't lost control of bot
		if (!char) {
			return Promise.reject(`${outcome.charId} not controlled`);
		}
		let { unitNr, target } = outcome;

		// Here we call the different commands directly against the API. We
		// sleep a little inbetween commands to avoid triggering the flood
		// filter, and to make it look more "natural".
		await char.call('address', {
			msg: "Sure thing, let me get that ready for you.",
			charId: target.id,
		});
		await sleep(1000);
		await char.call('useExit', { exitKey: 'out' });
		await sleep(1000);
		await char.call('useExit', { exitKey: 'up' });
		await sleep(1000);
		let area = await char.call('createArea', {
			name: `Apartment ${unitNr}`,
			ParentID: char.inRoom.area.id
		});
		await sleep(1000);
		await char.call('setLocation', {
			locationId: area.id,
			type: 'area',
			private: true
		});
		await sleep(1000);
		let createExitResult = await char.call('createExit', {
			keys:  [ unitNr, target.name + " " + target.surname ],
			name: `Apartment ${unitNr}`,
			leaveMsg: `goes inside apartment ${unitNr}.`,
			arriveMsg: "enters the apartment from the hallway.",
			travelMsg: `goes inside apartment ${unitNr}`,
			hidden: true
		});
		await sleep(1000);
		await char.call('useExit', { exitKey: unitNr });
		await sleep(1000);
		await char.call('setRoom', {
			name: `Apartment ${unitNr}`,
			desc: "The apartment is empty.",
			areaId: area.id
		});
		await sleep(1000);
		await char.call('setExit', {
			exitKey: 'back',
			name: 'To Hallway',
			keys: [ 'exit', 'out', 'hall', 'hallway' ],
			leaveMsg: "leaves the apartment.",
			arriveMsg: `arrives from apartment ${unitNr}.`,
			travelMsg: "leaves the apartment."
		});
		await sleep(2000);
		await char.call('requestSetRoomOwner', {
			roomId: createExitResult.targetRoom.id,
			charId: target.id
		});
		await char.call('requestSetAreaOwner', {
			areaId: area.id,
			charId: target.id
		});
		await sleep(1000);
		await char.call('teleportHome');
		await sleep(3000);
		await char.call('address', {
			msg: replaceTags("says ,\"Alright, you’re all set up with your new apartment. Here are your keys, you’re in unit {unitNr} Thank you for choosing Cinnabar Prism Apartments, we hope you enjoy your stay. Feel free to have a look around the facilities.\"\n((You can get there with the commands: `go out`, `go up`, `go apartment {unitNr}` (or alternatively `go {charName} {charSurname}` or simply `go {unitNr}`) ))\n((Make sure to accept the room and area requests in the Realm panel to the far left.))", {
				unitNr,
				charName: target.name,
				charSurname: target.surname
			}),
			pose: true,
			charId: target.id
		});

	}

	_alreadyHasApartment(charId) {
		// Check the JSON file/database if the char ID already has an apartment.
		return false;
	}

	_getNextApartmentNumber() {
		// Get next apartment from JSON file/database
		this.nextApartmentNumber = (this.nextApartmentNumber || 0) + 1;
		return this.nextApartmentNumber;
	}

	dispose() {
		this.module.charEvents.unsubscribe(this._onCharEvent);
		this.module.botController.removeAction('createApartment');
	}

}

export default ReactionApartmentRequest;

Wow, thank you so much for doing all the heavy lifting here. It did take me longer than I wanted to even get down the json storage… I’m not a JS programmer…

One thing of note: where in the config file do I need to REPLACE_WITH_BOT_ID I don’t know if I’m blind or what. But besides that it’s time to debug :grimacing:. If all goes well the complex can open up very soon! :tada:

Oh! I ended up pasting a config version without it!
Now I’ve edit it. It is to tell botController which characters it should listen to events on, and try to perform actions for (like waking up.). By setting it to the bot, botController will ignore the other chars.

1 Like

So… Any reason why when I start the bot I get the error:

Starting bot failed: SyntaxError: Invalid URL: /ws
    at initAsClient (<redacted>\mucklet-bot\node_modules\ws\lib\websocket.js:625:13)
    at new WebSocket (<redacted>\mucklet-bot\node_modules\ws\lib\websocket.js:83:7)
    at ResClient.Api._client.ResClient.namespace.namespace [as wsFactory] (file:///<redacted>/mucklet-bot/modules/api/Api.js:24:38)
    at connectPromise (C:\<redacted>\mucklet-bot\node_modules\resclient\lib\class\ResClient.js:260:28)
    at new Promise (<anonymous>)
    at ResClient.connect (<redacted>\mucklet-bot\node_modules\resclient\lib\class\ResClient.js:255:59)
    at Api.connect (file:///<redacted>/mucklet-bot/modules/api/Api.js:40:23)
    at Login._tryGetUser (file:///<redacted>/mucklet-bot/modules/login/Login.js:54:26)
    at Login.getUserPromise (file:///<redacted>/mucklet-bot/modules/login/Login.js:50:54)
    at Player._tryGetPlayer (file:///<redacted>/mucklet-bot/modules/player/Player.js:60:28)

I made sure I had all of the node modules installed, but it seems to not like the websocket protocol url? :man_shrugging:

Yes, the reason is missing the api modules hostUrl setting. It defaults to the relative URL /ws, which doesn’t work because node.js has no website to be relative to.

Either you can edit the configuration site:

API settings for mucklet.com test server

api: {
	hostUrl: "wss://test.mucklet.com",
	webResourcePath: "https://test.mucklet.com/api/",
	origin: "https://mucklet.com",
},

API settings for wolfery.com

api: {
	hostUrl: "wss://api.wolfery.com",
	webResourcePath: "https://api.wolfery.com/api/",
	origin: "https://wolfery.com",
},

Or you set it using a command flag:

node index.js --api.hostUrl=wss://api.wolfery.com --api.origin=https://wolfery.com --api.webResourcePath=https://api.wolfery.com/api/ devcfg/config-chippy.json

(Actually the webResourcePath is not used by the bot code currently, so it can be ignored. Everything is done over the WebSocket)

Alrighty.

I’ve just got a couple kinks I need to work out with learning/using level, then a final few tests of making sure Chippy can make rooms and hand them off properly, and I’ll be ready for the grand opening!

1 Like

Does that strike through mean we are ready to attach it to Sinder, and to have a grand opening? :smiley:

Possibly :wink:. I think I have things ready. But Chippy’s design is definitely not idiot proof. (for instance, for some reason if you request and receive a new apartment, then ask again before the bot restarts, it approved the request again and tries to make another apartment for the user, despite them having an entry in the db) For now I have a sign in the room to play nice, but as Wolfery grows, I’ll need to make him more robust to trolls, bad actors, and foul play, both intentional and unintentional.

I think it has to do with how things are written in Level, as I believe the stored ‘next’ number is properly updated, and multiple characters can order in succession, but if someone tries twice in a row they’ll end up with two with the second one failing because of the overlapping exit name.

I’ve got some things to work out on my end in terms of hosting Chippy, but once that’s settled then I think yes, we’re ready to attach it to Sinder!

Oh and I also had to raise the sleep commands from a second to 5 between commands as I was being rate limited, so that feature works :grin:. At some point I’ll try to hone in and tighten the timings but for now it works.

Cool!

I think Level will store instantly. I have another guess:

In ReactionApartmentRequest._onCharEvent (if you haven’t renamed it), you check if the character has an apartment since before, or else Chippy will tell you ‘No’.

Now, if no previous apartment is found, _onCharEvent will enqueue the action of Chippy creating a new one. My guess is that it isn’t until somewhere in _exec (the execution of the action) that you store the apartment number for that character, preventing additional requests. But then it is too late, and multiple requests might already have been queued.

If so, the fix would be to write to Level directly in _onCharEvent, as soon as you’ve validated the character doesn’t have a previous apartment. But since you don’t have the apartment number at that time, you can just write some other placeholder, eg: “toBeCreated”. And once created, you replace it with the real apartment number.

Or if something fails for Chippy, the Level entry can be deleted.

Actually, you can calculate exactly what sleep you need.
Players has a fixed set of “time” to spend. It is currently hardcoded to 100 seconds (for the core service, which is the one handling rooms and areas).

Each command spends a set amount of “time”. And every second, the player recovers one second, with a max of 100 seconds when fully idled. If you reach 0, you will get the flood error.

The cost of the different commands are:

  • Room communication (address, say, pose, describe) - 7 seconds
  • Private communication (whisper, message) - 5 seconds
  • Move (go/useExit, teleport, home) - 5 seconds
  • Create item (createRoom, createExit, createArea) - 10 seconds
  • Set item (setRoom, setArea, setLocation) - 10 seconds
  • Request (requestSetRoomOwner, requestSetAreaOwner) - 15 seconds

With that info, you can sum up the total of commands that Chippy will perform.

I got it to a total of 114 seconds for all of Chippy’s steps.
That means, a fully “rested” Chippy would require 14 additional seconds of sleep in between steps to perform the entire sequence.

And this means, you can keep track of this, and have Chippy say that he’ll be right at it; he just needs to recharge for a moment.
And then you can queue the next createApartment actionwith an X seconds delay (calculated based on last apartmentcreation) before he starts his action. :sunglasses:

Of course they do… you built it, you’d know how much everything costs :man_facepalming:. Thank you.

I’ve done a simple-ish system that accounts for all of Chippy’s bot actions (which won’t account for things that I enter in manually, but I think it’ll be fine :blush:. It then checks if it needs to wait before sending an action to make sure there are enough seconds available to work with, and waits the difference if there isn’t.

The only message I have that mentions this is at the end of an apartment creation, as the whole process is set to wait until full, then use it all, putting him in the most wait afterwards, but allowing the least amount of checks during (none). If waiting becomes an issue I can add more messages to let user’s know he’s waiting to not get rate limited.

I think down the road I may want to add a few more checks, like making sure he only takes requests in the management office and making sure he can’t request himself (I’ve already done it accidentally a few times :sweat_smile:. But besides stress testing I think he’s ready for deployment. At the moment I don’t have anywhere to run the bot indefinitely, but after I settle some personal difficulties with my home NAS I can dockerize the bot and run it there.

I think it is awesome, what you’ve done!
I look forward seeing Chippy in action, and to explore the new Prism Apartments.

Just tell me (or request to me) when ready, and we’ll have it linked, and a sign put up in the park about it!

And, if you find the rate limitation too strict, or some of the actions too expensive, I don’t mind adjusting them.

@Accipiter I’ve send the requests for the exit and parent area. I’m looking today to get Chippy deployed on a free-tier instance on Google Cloud or something similar while I wait for Seagate to get me my hard drives back.

1 Like