Symposium's secure trading system. ---------------------------------- Ever had someone steal something from you under the pretense of "having a look"? This might just be the solution. This is based on the trading interface that exists in Diablo II, without the loophole. This code allows for secure trading, a trade interface which allows you to display your goods and agree on a trade before transferring anything. The interface is as simple as I can imagine it, without having any security loopholes. The basic concept is that a player opens a trade session with someone else, they add items and gold as part of their half of the trade deal and through a two step (lock and agree) process, the trade is completed. The lock stage is a necessary part of the deal so that both parties can examine the goods without any alterations, once the deal is locked, you can be certain that no further changes have been made to either party's offer. -- Installation I'll assume that you'll know how to declare the functions used here, as well as how to add a new command into the interpreter. Once you have done those things, you will be able to recompile and use the new command. This should work with merc derivatives, although I couldn't be certain, it's been a long time since my code has looked anything like a merc mud. ------- This is the help that I wrote in a few quick seconds. The secure trading interface allows you to transfer goods with complete safety, no-one can pinch your stuff unless you let them. If you don't trust someone completely, using this interface can ensure that they don't cheat you. To start a trading session the first player must offer to trade. trade offer [player] The player target is optional, if you leave this off, any other player may join the session. To join a session type: trade join At any time you may leave a trade by typing: trade withdraw Once the session has started you may type 'trade' or 'trade examine' to view the current state of the trade. To view details on any item in the trade, type: trade examine To add gold or items to the trade type: trade add | If you enter a number it will subtract the gold from what you carry, this will be returned to you if you withdraw. Objects and gold may be removed by using: trade remove | In order to ensure that no changes are made you may lock the session. Once locked neither party may change what is on offer, while locked you may both examine the goods carefully. trade lock This command toggles the lock setting, you can check if the trade is locked by typing 'trade'. Use the trade lock in order to give yourself time to examine the other party's offer. In order to finalise the deal both parties must have locked the session, you cannot affect the lock set by the other party. Both parties must also accept the deal by typing: trade accept Once both parties have accepted the trade, the items are transferred and the trade is over. ------- Add the following to merc.h as you see fit: /* * Secure trading #defines. */ #define MAX_TRADE_OBJ 3 #define TRADE_LOCKED 1 #define TRADE_AGREED 2 /* * Structures for the securing trading interface. * by Symposium. */ typedef struct trade_data TRADE_DATA; struct trade_half { CHAR_DATA *ch; int flags; OBJ_DATA *objs[MAX_TRADE_OBJ]; int gold; }; struct trade_data { struct trade_half first; struct trade_half second; }; ------- Add the following line to the struct pc_data TRADE_DATA *trade; ------- Add the following code to act_???.c anywhere: #define IS_TRADE_LOCKED( _ch ) \ ( IS_SET( (_ch)->pcdata->trade->first.flags, TRADE_LOCKED ) \ || IS_SET( (_ch)->pcdata->trade->second.flags, TRADE_LOCKED ) ) #define GET_TRADE( _ch ) \ ( ( (_ch) == (_ch)->pcdata->trade->first.ch ) \ ? (_ch)->pcdata->trade->first \ : (_ch)->pcdata->trade->second ) /* * Display an offer of trade from victim to ch. */ void show_trade( CHAR_DATA *ch, CHAR_DATA *victim ) { OBJ_DATA *obj; char buf[MAX_INPUT_LENGTH]; int i; const char *kw; sprintf( buf, "Gold: %d coins.\n\r", GET_TRADE( victim ).gold ); send_to_char( buf, ch ); for( i = 0; i < MAX_TRADE_OBJ; ++i ) { obj = GET_TRADE( victim ).objs[i]; if( !obj ) continue; sprintf( buf, "Object: %s\n\r", obj->short_descr ); send_to_char( buf, ch ); } if( ch == victim ) kw = "have"; else kw = "has"; if( IS_SET( GET_TRADE( victim ).flags, TRADE_AGREED ) ) act( "$N $t agreed to the trade.", ch, kw, victim, TO_CHAR ); else if( IS_SET( GET_TRADE( victim ).flags, TRADE_LOCKED ) ) act( "$N $t set a lock on the trade.", ch, kw, victim, TO_CHAR ); return; } /* * Finds a trade object. */ OBJ_DATA *find_trade_obj( CHAR_DATA *ch, CHAR_DATA *victim, const char *name ) { OBJ_DATA *obj; int i; for( i = 0; i < MAX_TRADE_OBJ; ++i ) { obj = GET_TRADE( victim ).objs[i]; if( obj && is_name( name, obj->name ) ) return obj; } return NULL; } /* * Checks that the items offered in trade still exist. */ bool check_trade( CHAR_DATA *ch ) { OBJ_DATA *obj; int i; for( i = 0; i < MAX_TRADE_OBJ; ++i ) { obj = GET_TRADE( ch ).objs[i]; if( !obj ) continue; if( obj->carried_by != ch ) return FALSE; } return TRUE; } /* * Move items in trade from victim to ch. * * Note that for security reasons greater than mundane weight considerations, * I have not considered weight important. */ void affect_trade( CHAR_DATA *ch, CHAR_DATA *victim ) { OBJ_DATA *obj; int i; for( i = 0; i < MAX_TRADE_OBJ; ++i ) { obj = GET_TRADE( victim ).objs[i]; if( !obj ) continue; act( "You give $p to $N.", victim, obj, ch, TO_CHAR ); act( "$n gives $p to you.", victim, obj, ch, TO_VICT ); act( "$n gives $p to $N.", victim, obj, ch, TO_NOTVICT ); obj_from_char( obj ); obj_to_char( obj, ch ); } if( GET_TRADE( victim ).gold > 0 ) { act( "You give some gold to $N.", victim, NULL, ch, TO_CHAR ); act( "$n gives some gold to you.", victim, NULL, ch, TO_VICT ); act( "$n gives some gold to $N.", victim, NULL, ch, TO_NOTVICT ); } ch->gold += GET_TRADE( victim ).gold; return; } /* * Symposium's secure trade interface. */ void do_trade( CHAR_DATA *ch, const char *argument ) { CHAR_DATA *victim; OBJ_DATA *obj; char arg[MAX_INPUT_LENGTH]; int i; if( IS_NPC( ch ) ) return; argument = one_argument( argument, arg ); if( !ch->pcdata->trade ) { if( ch->mana < 100 + ch->level * 2 ) { send_to_char( "You need some more mana in order to initiate a trade.\n\r", ch ); return; } if( !str_cmp( arg, "offer" ) ) { ch->mana -= 100 + ch->level * 2; ch->pcdata->trade = (TRADE_DATA *)alloc_mem( sizeof( TRADE_DATA ) ); ch->pcdata->trade->first.ch = ch; ch->pcdata->trade->first.flags = 0; ch->pcdata->trade->first.gold = 0; if( ( victim = get_char_room( ch, argument ) ) && victim != ch && !IS_NPC( victim ) ) ch->pcdata->trade->second.ch = victim; else ch->pcdata->trade->second.ch = NULL; ch->pcdata->trade->second.flags = 0; ch->pcdata->trade->second.gold = 0; for( i = 0; i < MAX_TRADE_OBJ; ++i ) { ch->pcdata->trade->first.objs[i] = NULL; ch->pcdata->trade->second.objs[i] = NULL; } if( ch->pcdata->trade->second.ch ) act( "&c$n open$% an offer for a trade session with $N.", ch, NULL, victim, TO_ALL ); else act( "&c$n open$% an offer for a trade session.", ch, NULL, NULL, TO_ALL ); return; } if( !str_cmp( arg, "join" ) ) { if( !( victim = get_char_room( ch, argument ) ) || IS_NPC( victim ) ) { send_to_char( "That person is not here.\n\r", ch ); return; } if( victim->pcdata->trade == NULL ) { send_to_char( "They have not offered to trade.\n\r", ch ); return; } if( victim->pcdata->trade->second.ch != NULL && victim->pcdata->trade->second.ch != ch ) { act( "&r$N has restricted the trade so you can't join.", ch, NULL, victim, TO_CHAR ); return; } ch->mana -= 100 + ch->level * 2; ch->pcdata->trade = victim->pcdata->trade; ch->pcdata->trade->second.ch = ch; act( "&c$n join$% a trade session with $N.", ch, NULL, victim, TO_ALL ); return; } send_to_char( "Type HELP TRADE to get more information on trading securely.\n\r", ch ); return; } /* From here down a trade exists */ victim = (ch == ch->pcdata->trade->first.ch ) ? ch->pcdata->trade->second.ch : ch->pcdata->trade->first.ch; if( !str_prefix( arg, "examine" ) ) { if( argument[0] == '\0' ) { send_to_char( "&mYour offer is as follows:\n\r", ch ); show_trade( ch, ch ); act( "&m$N's offer is as follows:", ch, NULL, victim, TO_CHAR ); show_trade( ch, victim ); return; } if( !( obj = find_trade_obj( ch, victim, argument ) ) && !( obj = find_trade_obj( ch, ch, argument ) ) ) { send_to_char( "You can't find that object.\n\r", ch ); return; } send_to_char( "&gYou examine the object to determine its worth...&n\n\r", ch ); spell_identify( skill_lookup( "identify" ), ch->level, ch, obj ); return; } if( !str_prefix( arg, "add" ) ) { if( IS_TRADE_LOCKED( ch ) ) { send_to_char( "&rYou cannot make a change to a locked trade.&n\n\r", ch ); return; } if( is_number( argument ) ) { i = atoi( argument ); if( i <= 0 || i > ch->gold ) { send_to_char( "You have insufficient funds for that.\n\r", ch ); return; } if( ch == ch->pcdata->trade->first.ch ) ch->pcdata->trade->first.gold += i; else ch->pcdata->trade->second.gold += i; ch->gold -= i; sprintf( arg, "&yYou add %d gold to your trade.&n\n\r", i ); send_to_char( arg, ch ); sprintf( arg, "&y$n adds %d to $s trade.", i ); act( arg, ch, NULL, victim, TO_VICT ); return; } if( !( obj = get_obj_carry( ch, argument ) ) ) { send_to_char( "You can't find that item.\n\r", ch ); return; } if( !can_drop_obj( ch, obj ) ) { send_to_char( "You cannot offer that item.\n\r", ch ); return; } for( i = 0; i < MAX_TRADE_OBJ; ++i ) { if( GET_TRADE( ch ).objs[i] == NULL ) { GET_TRADE( ch ).objs[i] = obj; act( "&gYou add $p to the trade.", ch, obj, victim, TO_CHAR ); act( "&g$n adds $p to the trade.", ch, obj, victim, TO_VICT ); return; } } send_to_char( "You have used all of your slots for items, sorry.\n\r", ch ); return; } if( !str_prefix( arg, "remove" ) ) { if( IS_TRADE_LOCKED( ch ) ) { send_to_char( "&rYou cannot make a change to a locked trade.&n\n\r", ch ); return; } if( is_number( argument ) ) { i = atoi( argument ); if( i <= 0 || i > GET_TRADE( ch ).gold ) { send_to_char( "You cannot remove that amount!\n\r", ch ); return; } if( ch == ch->pcdata->trade->first.ch ) ch->pcdata->trade->first.gold -= i; else ch->pcdata->trade->second.gold -= i; ch->gold += i; sprintf( arg, "&yYou remove %d gold from your trade.\n\r", i ); send_to_char( arg, ch ); sprintf( arg, "&y$n removes %d from $s trade.", i ); act( arg, ch, NULL, victim, TO_VICT ); return; } if( !( obj = find_trade_obj( ch, ch, argument ) ) ) { send_to_char( "You can't find that item.\n\r", ch ); return; } for( i = 0; i < MAX_TRADE_OBJ; ++i ) { if( obj == GET_TRADE( ch ).objs[i] ) GET_TRADE( ch ).objs[i] = NULL; } act( "&gYou remove $p from the trade.", ch, obj, victim, TO_CHAR ); act( "&g$n removes $p from the trade.", ch, obj, victim, TO_VICT ); return; } if( !str_cmp( arg, "withdraw" ) || !str_cmp( arg, "leave" ) ) { send_to_char( "^gYou withdraw from the trade.&n\n\r", ch ); act( "&g$n withdraws from the trade.", ch, NULL, victim, TO_VICT ); ch->gold += GET_TRADE( ch ).gold; victim->gold += GET_TRADE( victim ).gold; free_mem( ch->pcdata->trade, sizeof( TRADE_DATA ) ); ch->pcdata->trade = NULL; victim->pcdata->trade = NULL; return; } if( !str_cmp( arg, "lock" ) ) { if( IS_SET( GET_TRADE( ch ).flags, TRADE_LOCKED ) ) { if( ch == ch->pcdata->trade->first.ch ) { REMOVE_BIT( ch->pcdata->trade->first.flags, TRADE_LOCKED ); REMOVE_BIT( ch->pcdata->trade->first.flags, TRADE_AGREED ); REMOVE_BIT( ch->pcdata->trade->second.flags, TRADE_AGREED ); } else { REMOVE_BIT( ch->pcdata->trade->second.flags, TRADE_LOCKED ); REMOVE_BIT( ch->pcdata->trade->second.flags, TRADE_AGREED ); REMOVE_BIT( ch->pcdata->trade->first.flags, TRADE_AGREED ); } send_to_char( "&GYou unlock the trade.&n\n\r", ch ); act( "&G$n unlocks the trade.", ch, NULL, victim, TO_VICT ); return; } send_to_char( "&GYou lock the trade.&n\n\r", ch ); act( "&G$n locks the trade.", ch, NULL, victim, TO_VICT ); if( ch == ch->pcdata->trade->first.ch ) SET_BIT( ch->pcdata->trade->first.flags, TRADE_LOCKED ); else SET_BIT( ch->pcdata->trade->second.flags, TRADE_LOCKED ); return; } if( !str_cmp( arg, "agree" ) || !str_cmp( arg, "accept" ) ) { if( !IS_SET( GET_TRADE( ch ).flags, TRADE_LOCKED ) || !IS_SET( GET_TRADE( victim ).flags, TRADE_LOCKED ) ) { send_to_char( "&rBoth parties must lock the trade first.&n\n\r", ch ); return; } if( IS_SET( GET_TRADE( ch ).flags, TRADE_AGREED ) ) { if( ch == ch->pcdata->trade->first.ch ) REMOVE_BIT( ch->pcdata->trade->first.flags, TRADE_AGREED ); else REMOVE_BIT( ch->pcdata->trade->second.flags, TRADE_AGREED ); send_to_char( "&GYou withdraw your agreement to the trade.&n\n\r", ch ); act( "&G$n removes $s agreement to the trade.", ch, NULL, victim, TO_VICT ); return; } if( !IS_SET( GET_TRADE( victim ).flags, TRADE_AGREED ) ) { if( ch == ch->pcdata->trade->first.ch ) SET_BIT( ch->pcdata->trade->first.flags, TRADE_AGREED ); else SET_BIT( ch->pcdata->trade->second.flags, TRADE_AGREED ); send_to_char( "&GYou signify your agreement for this trade.&n\n\r", ch ); act( "&G$n has agreed to the trade.", ch, NULL, victim, TO_VICT ); return; } if( !check_trade( ch ) || !check_trade( victim ) ) { send_to_char( "&rAn item is missing, the trade must be cancelled.&n\n\r", ch ); send_to_char( "&rAn item is missing, the trade must be cancelled.&n\n\r", victim ); ch->gold += GET_TRADE( ch ).gold; victim->gold += GET_TRADE( victim ).gold; free_mem( ch->pcdata->trade, sizeof( TRADE_DATA ) ); ch->pcdata->trade = NULL; victim->pcdata->trade = NULL; return; } affect_trade( ch, victim ); affect_trade( victim, ch ); free_mem( ch->pcdata->trade, sizeof( TRADE_DATA ) ); ch->pcdata->trade = NULL; victim->pcdata->trade = NULL; act( "&GYou shake $N's hand and seal the deal.", ch, NULL, victim, TO_CHAR ); act( "&G$n closes the deal, you shake $s hand and seal the deal.", ch, NULL, victim, TO_VICT ); act( "&G$n has just closed the deal with $N.", ch, NULL, victim, TO_NOTVICT ); return; } send_to_char( "Type HELP TRADE to get more information on trading securely.\n\r", ch ); return; } ------------ end main code segment The last thing to do is ensure that no-one quits while still in a trade session. Simply add the following line in the quit code somewhere and ensure that it is run every time. if( !IS_NPC( ch ) && ch->pcdata->trade ) do_trade( ch, "withdraw" ); -- Other You might like to add a few things yourself, these are what I thought of. Instead of using mana to cover the cost of identify, perhaps some form of ticket would be required in order to initiate a session. This would avoid people using trade sessions as a means of almost free, unlimited identifies. Also add a small amount of lag on the examine command to reduce its abuse. Possible solution: use examine, check if the character can identify and use that if they have it. Possible solution: charge a small amount of gold for each identify - this is my preferred solution. A loophole has come to my attention, this code could be used to exceed a player's weight limit, thus I suggest in the checking code above, a tally of the total weight of the items should be kept and the number of items, compare this with the current and maximum limits for the player and disallow the trade if the limits are exceeded. The code needs colourisation, especially the "trade examine" portion. You can add a small notice to the start of a trader's prompt in order to ensure that they are continually reminded of the fact that they are trading. Similarly, some form of notification in the long description of a character would be good. Trade offers should time out. This may require some form of event system in order to force withdrawal after a certain period, this would free the memory and also help counteract the aforementioned identify abuse. -- Contact/Credits This code was inspired by a little problem I had with another player recently. The idea came from the trade interface in Diablo II, but I have modified the interface somewhat in order to overcome some of the problems that exist with that model. I'd appreciate it if the comments remain in the code, I'm not looking for credits in the helps or greeting screen or anything, I think that this is a reasonable request. You can contact me at mwt02@uow.edu.au. Maybe you can let me know if you like the idea, or if you had an improvement to make.