MUDs em TCP
Aprenda como fazer um servidor MUD, em Delphi

Muitos dos conceitos de jogos multiplayer são aplicados também aos MUDs. Portanto, é extremamente recomendável a leitura do material sobre programação multiplayer do club TILT. Iremos considerar que ele tenha sido lido e devidamente executado.

Diferentemente de muitos jogos atuais, a conexão de um MUD é mantida através do protocolo TCP. Ao invés do UDP, o TCP mantém uma conexão contínua entre cliente e servidor, avisando imediatamente a conexão/desconexão de um usuário. Sua desvantagem em relação ao UDP é a velocidade: manter a conexão e assegurar que os dados cheguem ao destino intactos e na devida ordem custa valiosos micro-segundos, que podem inviabilizar jogos de alta velocidade. Mas como o ritmo de jogo de um MUD é bem inferior ao comum nos dias de hoje, a conexão TCP é a ideal.

Além do TCP, os MUDs usam também um protocolo de nível superior, chamado Protocolo de Terminal ou Telnet. Não vamos, no entanto, nos preocupar com isso, uma vez que estamos fazendo nosso próprio servidor (e possivelmente, como bons designers, nosso próprio cliente). Vamos direto ao que interessa.

Iniciando

Como foi dito, o MUD é um servidor TCP que envia e recebe única e exclusivamente texto (strings). Portanto, trabalhar com ele no Delphi será incrivelmente fácil. Um teste simples que pode ser feito para experimentar como funcionará o servidor é o seguinte: Em um novo projeto, crie um TServerSocket (aba internet) com as propriedades name (server), port (4000) e active (true).

No evento OnClientConnect desse componente, escreva:

socket.sendText('Alô MUnDo');

Rode o programa. Então execute o programa Telnet (já incluso no windows) na caixa "executar" do menu iniciar com o comando "telnet 127.0.0.1 4000". Se tudo der certo o texto "Alô MUnDo" aparecerá escrito na janela do terminal.

Este artigo poderia terminar por aqui, já que agora o processo de criar o MUD se tornou nada mais que um exercício de programação: receber dados pelo evento OnClientRead e responder apropriadamente. Porém vamos estudar um pouco mais a estrutura básica do jogo, criando um sistema que permita movimentação entre salas, conversa entre os jogadores conectados e coleta de itens - elementos esseciais à esse tipo de jogo.

Estruturando o servidor

O roteiro básico de uma sessão de jogo é conexão-jogo-desconexão. A parte "jogo" é a maior e mais complexa, portanto iremos começar criando nossas rotinas de conexão e desconexão. Elas devem ser capazes de apresentar informações básicas aos jogadores e preparar o terreno para o jogo (incluindo aí criar variáveis para as informações do usuário).

As informações do jogador serão guardadas em um record de nome TJogador. Ele será definido como:

TJogador = record
  nome: string; //nome do jogador
  senha: string; //senha
  posicao: string; //a localização dele no jogo
  inventario: string; //os itens que ele têm
  socket: TCustomWinSocket; //o objeto responsável pela 
                            //conexão TCP
  cmd: string; //o comando sendo digitado
  flag: integer; //indica que posição da conexão ele está
end;

Entre as variáveis do jogador, é preciso destacar o Socket, que guarda as informações da conexão entre o servidor e o cliente; Cmd que é o comando sendo digitado pelo usuário (uma vez que os comandos pelo terminal vêm letra por letra conforme são escritos) e flag, que indica em que ponto da conexão o jogador está (uma vez que o MUD tem uma execução linear, precisamos saber se o jogador está agora mandando o login, a senha ou comandos de jogo).

Agora serão definidas algumas constantes que controlarão a variável flag do jogador:

const
  FLDESCONHECIDO = 0; //flag desconhecida
  FLLOGIN = 1; //jog mandando nome
  FLSENHA = 2; //jog mandando senha
  FLNSENHA1 = 3; //novo jog mandando senha
  FLNSENHA2 = 4; //novo jog repetindo senha
  FLANDANDO = 5; //jogando

E finalmente a variável global que controlará o registro dos jogadores:

var
  jogs: array of TJogador;

                    

Esse tipo de variável (um array dinâmico) pode comportar qualquer número de elementos (jogadores) à qualquer momento. Ele é dimensionado usando-se a função "SetLength" que aloca ou libera memória conforme a necessidade.

No momento em que uma conexão for criada, essa lista deve ser aumentada, e uma ligação entre a conexão TCP e o novo elemento da lista deve ser estabelecida, para que mais tarde seja possível encontrar o jogador através da conexão. Portanto, o evento onClientConnect deve ficar:

procedure TForm1.serverClientConnect(Sender: TObject;
    Socket: TCustomWinSocket);
var
  jog: integer;
  p: Pointer;
begin
  jog:= length(jogs);
  setLength(jogs, jog + 1); //aumentar lista
  jogs[jog].flag:= FLLOGIN; //indicar posição do jogador
  socket.SendText(BemVindo.Text); //texto de conexão
  getMem(p, 4);
  integer(p^):= jog;
  socket.Data:= p; //ligar jogador e socket
  jogs[jog].socket:= socket; 
end;

Primeiramente, a lista de jogadores é aumentada e os dados iniciais (flag) do jogador são estabelecidos. Em seguida, um texto de boas vindas é enviado ao jogador ("BemVindo" é um TMemo que contém alguma mensagem do tipo "Bem vindo ao meu MUD! Qual é o seu nome?"). Logo após, é feita uma pequena "mágica" com ponteiros para ligar a conexão à variável do jogador recém criada.

O campo "data" do TCustomWinSocket é um ponteiro que pode ser usado livremente por qualquer aplicação. O que faremos é ligar a conexão (o socket) com a variável do jogador (jogs[jog]) por meio desse campo Data. Para isso separamos na memória 4 bytes (cujo endereço estará nessa variável data) que guardarão o índice do jogador na lista (a variável local "jog"). Após alocar a memória e guardar o valor nesse local o inverso é feito e a variável socket do jogador passa a guardar o socket da conexão.

Agora é a vez da desconexão. O evento OnClientDisconnect fica:

procedure TForm1.serverClientDisconnect(Sender: TObject;
    Socket: TCustomWinSocket);
var
  jog: integer;
  i: integer;
begin
  jog:= integer(socket.data^); //achar jogador
  if fileExists(jogs[jog].nome) then salvarJog(jog);//salvar
  freeMem(socket.data); //liberar memória
  //copiar os jogadores da frente da lista para trás
  for i:= jog to length(jogs) -2 do begin 
    jogs[i]:= jogs[i + 1];
    integer(jogs[i].socket.Data^):= i;
  end;
  setLength(jogs, length(jogs) -1); //reduzir lista
end;

Primeiro o índice do jogador é recuperado (da variável data do socket). Então suas informações são salvas em um arquivo (para que seu equipamento e sua posição possam ser recuperados quando reconectar) e a variável que guardava o índice é liberada. Em seguida os jogadores que estavam na frente desse jogador na lista são movidos para trás (ajustando-se seu índice) e finalmente a lista é diminuída.

O procedimento SalvarJog (que deve ser declarado nas definições do formulário) é o seguinte:

procedure TForm1.SalvarJog(jog: integer);
var
  stl: TStringList;
begin
  stl:= TStringList.create;
  stl.Add(jogs[jog].senha);
  stl.add(jogs[jog].posicao);
  stl.add(intToStr(jogs[jog].flag));
  stl.add(jogs[jog].inventario);
  stl.SaveToFile(jogs[jog].nome);
  stl.free;
end;

Ele simplesmente cria um TStringList, adiciona as informações do jogador e salva em um arquivo com seu nome para futura recuperação.

Com isso completamos dois dos três passos básicos na criação de um servidor MUD. Essas duas áreas do código serão pouco modificadas no decorrer do desenvolvimento, já que a maior parte da programação está na terceira etapa, o jogo em si.

Estruturando o jogo

Estabeleceremos que cada função do jogo (pegar um objeto, ver o conteúdo do inventário, etc) ficará em um procedimento em separado (para facilitar a localização e compreensão do código). Além disso, cada uma das diversas etapas do jogo (diferenciados pela variável flag) também terão seus próprios procedimentos. Começando pelo evento OnClientRead (que ocorre sempre quando alguma informação chega da máquina do usuário):

procedure TForm1.serverClientRead(Sender: TObject;
    Socket: TCustomWinSocket);
var
  cmd: string;
  jog: integer;
begin
  cmd:= socket.ReceiveText; //informação sendo recebida
  jog:= integer(socket.data^); //acha o índice do jogador
  if cmd = #13#10 then begin //se for <enter>
    //executar função relativa à flag
    case jogs[jog].flag of FLLOGIN: cmdLogin(jog);
    end;
    jogs[jog].cmd:= '';
  end else if cmd = #8 then //se for <backspace>
    jogs[jog].cmd:= copy(jogs[jog].cmd, 1,
                      length(jogs[jog].cmd) -1)
  else //se for qualquer outro caracter
    jogs[jog].cmd:= jogs[jog].cmd + cmd;
end;

Esse evento irá cuidar dos comandos das flags do jogador. Em primeiro lugar, recebemos o texto e achamos qual jogador está enviando esse comando. Se o comando enviado for um enter (caracteres #13#10) então testamos a flag do jogador, executamos o procedimento indicado e limpamos a variável de comando. Se a instrução for um backspace (para limpar o último caracter digitado) copiamos o comando digitado até agora menos a última letra. Se não for um desses anteriores, adicionamos o texto recebido ao comando corrente do jogador.

Por enquanto, apenas a flag FLLOGIN foi usada, e portanto o procedimento cmdLogin deve ser criado nas declarações do formulário ("procedure cmdLogin(jog: integer);") e implementado como:

procedure TForm1.cmdLogin(jog: integer);
var
  stl: TStringList;
begin
  jogs[jog].nome:= jogs[jog].cmd; //pegar nome
  //se o jogador existe, carregar arquivo e pedir senha
  if fileExists(jogs[jog].nome) then begin
    stl:= TStringList.create;
    stl.LoadFromFile(jogs[jog].cmd);
    jogs[jog].senha:= stl[0];
    jogs[jog].posicao:= stl[1];
    jogs[jog].flag:= FLSENHA;
    jogs[jog].inventario:= stl[3];
    jogs[jog].socket.SendText(#13#10 + 
                  'Jogador existente. Digite sua senha: ');
    stl.free;
  end else begin //senão pedir nova senha
    jogs[jog].flag:= FLNSENHA1;
    jogs[jog].socket.SendText(#13#10 + 
                       'Novo jogador. Digite sua senha: ');
  end;
end;

O nome do jogador é transportado da variável Cmd para a variável Nome. Então, caso um arquivo com o nome do jogador já exista, ele é carregado com suas informações e a senha é pedida ao usuário. Caso esse seja um novo jogador, uma nova senha é pedida (usamos flags diferentes porque a senha será pedida duas vezes no caso de um novo jogador).

O próximo passo são as senhas. Teremos três procedimentos diferentes para os três tipos de senhas (senha de um jogador existente, senha de um novo jogador ou repetição da senha de um novo jogador). Começando pela senha de um jogador existente:

procedure TForm1.cmdSenha(jog: integer);
begin
  if jogs[jog].senha = jogs[jog].cmd then begin
    //se a senha está correta
    jogs[jog].flag:= FLANDANDO;
    jogs[jog].socket.SendText(#13#10 + 'Jogador Conectado!');
  end else begin
    //se a senha está errada
    jogs[jog].flag:= FLLOGIN;
    jogs[jog].socket.SendText(#13#10 + 
                       'Senha incorreta! Digite seu nome: ');
  end;
end;

Se a senha do jogador (existia e foi carregada no momento do login do jogador) for igual à senha digitada, conectamos o jogador e mandamos uma pequena mensagem informando o sucesso da conexão. Se as senhas forem diferentes, o processo de login é reiniciado e o nome do usuário é pedido novamente.

Agora os procedimentos de nova senha:

procedure TForm1.cmdNSenha1(jog: integer);
begin
  jogs[jog].senha:= jogs[jog].cmd;
  jogs[jog].flag:= FLNSENHA2;
  jogs[jog].socket.SendText(#13#10 +
                          'Digite sua senha novamente: ');
end;
procedure TForm1.cmdNSenha2(jog: integer);
begin
  if jogs[jog].senha <> jogs[jog].cmd then begin
    //se forem diferentes
    jogs[jog].flag:= FLNSENHA1;
    jogs[jog].socket.SendText(#13#10 + 
                   'Senhas diferentes. Digite sua senha: ');
  end else begin
    //se forem iguais
    jogs[jog].flag:= FLANDANDO;
    jogs[jog].posicao:= '0';
    salvarJog(jog);
    jogs[jog].socket.SendText(#13#10 + 'Jogador criado!');
  end;
end;

Na primeira vez que um novo jogador digita a senha (cmdNSenha1) ela é guardada na variável respectiva; a flag é alterada para requisitar a segunda senha e mandamos uma mensagem.

Na segunda vez que a senha é digitada, ela é comparada com o valor anterior. Se forem diferentes o processo reinicia com o pedido da primeira senha. Se forem iguais, o jogador é conectado, colocado na posição ' 0 ' (mais sobre isso em um momento), suas informações são salvas e uma mensagem é mandada.

Para habilitar o uso dos procedimentos descritos, é preciso modificar o evento OnClientRead adicionando os três novos métodos das flags:

  //executar função relativa à flag
  case jogs[jog].flag of
    FLLOGIN: cmdLogin(jog);
    FLSENHA: cmdSenha(jog);
    FLNSENHA1: cmdNSenha1(jog);
    FLNSENHA2: cmdNSenha2(jog);
  end;

E faça-se o texto

O servidor criado até agora já apresenta as características básicas de qualquer jogo multiplayer: Conecta e desconecta jogadores e guarda em disco os dados necessários para uso futuro. Agora vêm a parte divertida, a implementação do jogo.

A primeira função a ser adicionada será a de visualizar o local atual do jogador. Portanto, será preciso guardar as informações dos diversos locais do MUD (como descrição, objetos no local, monstros, etc) em arquivos. Como padrão, os arquivos de locais serão salvos exclusivamente como números, sendo o primeiro local "0", o segundo "1" e assim por diante.

As definições de locais devem conter uma descrição, informações sobre saídas (qual comando para sair e para que posição o personagem se desloca ao executar esse comando) e informações sobre itens (já que nesse artigo implementaremos até funções de pegar/soltar objetos).

A estrutura do arquivo de locais será então:

<descrição>
<descrição>
<...>
<descrição>
 * (demarca o fim da seção de descrições)
 #,<saída>,<local-destino>
 +,<item>

A descrição do local começa na primeira linha do arquivo e vai até o asterisco. Nesse ponto começam as definições de saídas (começam pelo carectere # seguido pela saída e pelo local-destino dessa saída) e itens (começando pelo caractere + seguido pelo nome do item).

Serão precisos alguns locais para testarmos nosso jogo. Junto com o código desse artigo, encontram-se três locais (arquivos "0", "1" e "2) porém não existem limites para a criação de outros.

Como os comandos do MUD são textuais, e normalmente na forma (<comando> <parâmetro1> ... <parâmetro n>), será necessário uma função para separar o comando e os diversos parâmetros. Essa função necessitará de um tipo especial chamado TParsedString, que deve ser definido antes da declaração do forumlário:

type
  TParsedString = record //tipo para separação de strings
  strings: array of string;
  count: integer;
end;
TForm1 = class(TForm)
  ...

E a função de separação é:

//divide uma string em um array de strings,
//separados pelo "caract"
function ParseString(str, caract: string): TParsedString;
var
  p1: integer;
begin
  result.count:= 0;
  while true do begin
    p1:= pos(caract, str);
    if (p1 = 0) or (p1 = -1) then break;
    setLength(result.strings, result.count + 1);
    result.strings[result.count]:= copy(str, 1, p1 -1);
    str:= copy(str, p1 + 1, length(str));
    result.count:= result.count + 1;
  end;
  if length(str) > 0 then begin
    setlength(result.strings, result.count + 1);
    result.strings[result.count]:= str;
    result.count:= result.count + 1;
  end;
end;

O que esta função faz é procurar um determinado caracter separador em uma string, e dividi-la sempre que esse caracter é encontrado. Portanto a string "#,leste,1' quando dividida pela vírgula é retornada nos índices [0] = #, [1] = leste e [2] = 1 da variável Strings do tipo TParsedString. Essa função será muito útil durante a criação do jogo.

Com as funções prontas, é preciso implementar a função da flag de jogo (FLANDANDO). Inicialmente, ela será a seguinte:

procedure TForm1.cmdAndando(jog: integer);
var
  str: TParsedString;
begin
  //se for um comando em branco
  if jogs[jog].cmd = '' then exit;
  //separar pelos espaços
  str:= parseString(jogs[jog].cmd, ' ');
  //se for "olhar" para a posição
  if jogs[jog].cmd = 'olhar' then mandarPos(jog);
end;

O comando é testado, e caso seja vazio a função não é executada. Então, ele é separado pela barra de espaço (embora não usado no momento, será necessário em breve e portanto já foi incluído nessa listagem) e finalmente, de acordo com o comando mandado uma função diferente é executada.

A primeira função será a de visualizar um local (MandarPos) que manda ao jogador sua posição atual. Por partes:

procedure TForm1.mandarPos(jog: integer);
var
  stl: TStringList;
  lin: integer;
  s: string;
  str: TParsedString;
begin
  stl:= TStringList.create;
  stl.loadFromFile(jogs[jog].posicao); //carregar posição
  lin:= 0;
  jogs[jog].socket.sendText(#13#10); //pular linha
  while stl[lin] <> '*' do begin //mandar descrição até o fim
    jogs[jog].socket.SendText(stl[lin] + #13#10);
    lin:= lin + 1;
  end;

Definimos as variáveis que vamos precisar durante a função. Então um TStringList é criado e o local do jogador é carregado. Linha por linha, a descrição do local é mandada até que seja encontrado o caracter * que indica o fim das descrições. A segunda parte da função obtém e manda as saídas:

  lin:= lin + 1;
  s:= 'Saídas: '; //pegar saídas
  if lin < stl.count then
  while stl[lin][1] = '#' do begin
    str:= parseString(stl[lin], ',');
    s:= s + '[' + str.strings[1] + ']';
    lin:= lin + 1;
    if lin >= stl.count then break;
  end;
  jogs[jog].socket.sendText(s + #13#10); //mandar saídas

Começando na linha após o asterisco, checamos linha à linha pelo caracter #. Enquanto ele for encontrado (indicando que a linha descreve uma saída) o texto é separado e o nome da saída é concatenado na variável "s" (que é mandada ao jogador após todas as saídas terem sido achadas).

A última parte é a criação da lista de objetos:

  s:= 'Itens: ';
  if lin < stl.count then //pegar itens
  while stl[lin][1] = '+' do begin
    str:= parseString(stl[lin], ',');
    s:= s + '[' + str.strings[1] + ']';
    lin:= lin + 1;
    if lin >= stl.count then break;
  end;
  jogs[jog].socket.sendText(s + #13#10); //mandar itens
  stl.free;
end;

Esse trecho é muito semelhante ao anterior. Enquanto objetos (caractere +) forem achados, as linhas do texto são divididas e o objeto é concatenado na lista que é enviada ao jogador.

Para finalizar a exibição de locais, o método cmdAndando precisa ser adicionado ao evento OnClientRead, que agora permanecerá como:

  case jogs[jog].flag of
    FLLOGIN: cmdLogin(jog);
    FLSENHA: cmdSenha(jog);
    FLNSENHA1: cmdNSenha1(jog);
    FLNSENHA2: cmdNSenha2(jog);
    FLANDANDO: cmdAndando(jog);
  end;

Além disso, devem ser adicionadas chamadas ao método MandarPos assim que o jogador for conectado (nos procedimentos cmdSenha e cmdNSenha2) para que o usuário saiba imediatamente onde está sem a necessidade de um comando logo após conectar-se.

Até o momento os jogadores têm a capacidade de ver a descrição do local onde estão, as saídas e os itens presentes nessa posição. É preciso implementar a movimentação.

A rotina de movimentação será feita em uma função chamada TentarSair. Essa função deve carregar o local do jogador e verificar se o comando dado é ou não um comando para mover o personagem para outro local. Ela também retornará um valor booleano, indicando se o jogador foi ou não movido para outra posição, para que possamos parar de testar os comandos assim que descobrirmos qual a intenção do usuário.

function TForm1.TentarSair(jog: integer): boolean;
var
  stl: TStringList;
  lin: integer;
  str: TParsedString;
begin
  result:= false;
  stl:= TStringList.create;
  stl.loadfromfile(jogs[jog].posicao);
  lin:= stl.IndexOf('*') + 1;
  while stl[lin][1] = '#' do begin
    str:= parseString(stl[lin], ',');
    if jogs[jog].cmd = str.strings[1] then begin
      jogs[jog].posicao:= str.strings[2];
      MandarPos(jog);
      result:= true;
      break;
    end;
    lin:= lin + 1;
    if (lin > stl.count -1) or (result) then break;
  end;
  stl.free;
end;

Essa função começa carregando a posição atual do jogador. Então, começando na linha que precede o final da descrição, aquelas que descrevem uma saída são separadas, e caso o comando digitado pelo jogador seja o mesmo que o comando para saída o jogador é colocado na nova posição, a descrição dessa posição é mandada, e o processamento é interrompido.

Para que essa função comece seu trabalho, é preciso chamá-la do método cmdAndando, que ficará da seguinte maneira:

  //se for "olhar" para a posição
  if jogs[jog].cmd = 'olhar' then mandarPos(jog)
  //testar se é uma saída
  else if TentarSair(jog) then exit;

Note que caso o comando "olhar" seja digitado, a rotina é executada e o método cmdAndando é terminado. O mesmo ocorre com a função tentarSair: caso ela retorne verdadeiro (uma saída foi achada e o jogador foi movido) ela pára a checagem de qualquer outro comando, agilizando todo o processo.

Alguns comandos úteis

O ambiente virtual está pronto, mas ainda há muita pouca coisa para se fazer nele. Portanto, precisamos de alguns comandos simples para divertir os futuros jogadores, começando pelo comando "quem" para que qualquer um possa ver os jogadores conectados no momento. Essa função será muito simples:

procedure TForm1.MandarConectados(jog: integer);
var
  i: integer;
begin
  jogs[jog].socket.SendText('==== Lista de Conectados ==='
                                              + #13#10);
  for i:= 0 to length(jogs) -1 do
    jogs[jog].socket.SendText(jogs[i].nome + #13#10);
end;

Ela simplesmente corre a lista de jogadores, mandando para o jogador que requisitou a lista os nomes de todos os usuários conectados no momento.

Em seguida, implementaremos dois procedimentos que serão muito úteis na expansão do MUD, o método MandarTodos (que manda uma linha de texto para todos os usuários conectados) e AcharJog (que acha um jogador a partir de seu nome):

procedure TForm1.MandarTodos(s: string);
var
  i: integer;
begin
  for i:= 0 to length(jogs) -1 do
    jogs[i].socket.sendText(s);
end;
function TForm1.AcharJog(nome: string): integer;
var
  i: integer;
begin
  result:= -1;
  for i:= 0 to length(jogs) -1 do
  if jogs[i].nome = nome then begin
    result:= i;
    break;
  end;
end;

Logo após esses dois procedimentos, serão criadas duas funções para chat: Falar e Dizer. Falar será um comando para mandar uma linha de chat para todos os usuários conectados, enquanto Dizer mandará mensagens para um único jogador (os outros não verão essa conversa):

procedure TForm1.Falar(jog: integer);
var
  msg: string;
begin
  msg:= copy(jogs[jog].cmd, 7, length(jogs[jog].cmd));
  mandarTodos(jogs[jog].nome + ' falou "' + msg +
                                        '"' + #13#10);
end;
procedure TFOrm1.Dizer(jog: integer; str: TParsedString);
var
  dest: integer;
  s: string;
begin
  dest:= acharJog(str.strings[1]);
  if dest = -1 then begin
    jogs[jog].socket.SendText('Jogador não existe' +
                                             #13#10);
    exit;
  end;
  s:= copy(jogs[jog].cmd, 7 + length(str.strings[1]) + 1,
                                  length(jogs[jog].cmd));
  jogs[dest].socket.SendText(jogs[jog].nome + ' disse "' +
                                       s + '"' + #13#10);
end;

O primeiro procedimento é auto-explicativo: o comando do jogador virá na forma "falar xxxxx" onde xxxx é a mensagem a ser enviada. Portanto essa parte é copiada para a variável auxiliar "msg" e a linha de conversa é mandada para todos os jogadores.

No segundo procedimento, o primeiro passo é descobrir qual o jogador-destino da mensagem. Caso não exista um jogador com esse nome, é enviada uma mensagem de erro ao remetente. Caso contrário, a mensagem é enviada com sucesso.

O último trabalho é ajustar o procedimento cmdAndando para acomodar os novos comandos:

  //se for "olhar" para a posição
  if jogs[jog].cmd = 'olhar' then mandarPos(jog)
  //testar por saídas
  else if TentarSair(jog) then exit
  //lista de jogadores
  else if jogs[jog].cmd = 'quem' then MandarConectados(jog)
  //chat para todos os jogadores
  else if str.strings[0] = 'falar' then falar(jog)
  //chat para um jogador
  else if str.strings[0] = 'dizer' then dizer(jog, str);

Note que os comandos "falar" e "dizer" utilizam não a variável "jogs[jog].cmd" para checar o comando, mas a variável "str.strings[0]". Isso porque eles suportam diversos parâmetros, e apenas a primeira palavra identifica o comando adequado.

Coisas e mais coisas

Das metas estabelecidas no início do artigo, só falta o sistema de objetos e inventário. Serão implementados três métodos: Pegar (pega um objeto do local), MostrarInv (mostra a lista de itens que o jogador possui) e Soltar (solta um item do inventário para o local).

Começando pelo procedimento de pegar itens da posição atual:

procedure TForm1.Pegar(jog: integer; str: TParsedString);
var
  stl: TStringList;
  lin: integer;
  item: TParsedString;
begin
  stl:= TStringList.create;
  stl.loadFromFile(jogs[jog].posicao); 
  lin:= stl.IndexOf('*');
  for lin:= lin to stl.count -1 do
  if stl[lin][1] = '+' then begin
    item:= parseString(stl[lin], ',');
    if pos(str.strings[1], item.strings[1]) > 0 then begin
      stl.Delete(lin);
      stl.SaveToFile(jogs[jog].posicao);
      jogs[jog].inventario:= jogs[jog].inventario +
                                    item.strings[1] + ',';
      jogs[jog].socket.SendText('Você pegou ' +
                                item.strings[1] + #13#10);
      break;
    end;
  end;
  stl.free;
end;

O procedimento começa carregando a posição atual do jogador. A partir do fim das descrições, as linhas de objeto (indentificados pelo caractere +) são separadas. Se for detectado que o item que o jogador deseja pegar está presente nesse item do local, o item é deletado do local, a descrição é salva e o objeto é adicionado ao inventário do jogador.

Agora para a listagem do inventário:

procedure TForm1.MostrarInv(jog: integer);
var
  i: integer;
  str: TParsedString;
begin
  jogs[jog].socket.sendText('=== Inventário ===' + #13#10);
  //se o inventário estiver vazio
  if jogs[jog].inventario = '' then begin 
    jogs[jog].socket.sendText('<nada>' + #13#10);
    exit;
  end;
  str:= parseString(jogs[jog].inventario, ',');
  for i:= 0 to str.count -1 do //se tiver alguma coisa
  jogs[jog].socket.sendText(str.strings[i] + #13#10);
end;

O inventário é salvo de forma <item1>,<item2>,...,<itemn>. Portanto, caso existam items, a lista é separada e cada um dos objetos é mandado em uma linha diferente.

E finalmente, o procedimento Soltar:

procedure TForm1.Soltar(jog: integer);
var
  i, j: integer;
  inv: TParsedString;
  item: string;
  stl: TStringList;
begin
  inv:= parseString(jogs[jog].inventario, ',');
  item:= copy(jogs[jog].cmd, 8, length(jogs[jog].cmd));
  for i:= 0 to inv.count -1 do
  if pos(item, inv.strings[i]) > 0 then begin
    stl:= TStringList.create;
    stl.loadFromFile(jogs[jog].posicao);
    stl.Add('+,' + inv.strings[i]);
    stl.SaveToFile(jogs[jog].posicao);
    stl.free;
    jogs[jog].inventario:= '';
    for j:= 0 to inv.count -1 do
    if j <> i then jogs[jog].inventario:=jogs[jog].inventario
                                 + inv.strings[j] + ',';
    jogs[jog].socket.SendText('Você soltou ' + inv.strings[i]
                                                 + #13#10);
    break;
  end;
end;

A lista de inventário é separada. Em seguida ela é percorrida até que o item que o jogador deseja soltar seja encontrado. Quando isso acontece, o arquivo com a descrição do local é carregado, uma linha é adicionada para o novo objeto e ele é salvo e fechado. Então, um novo inventário é montado com os itens restantes e o processamento é terminado.

Para habilitar essas três funções, o procedimento cmdAndando deve ser novamente alterado, dessa vez de forma definitiva para:

  //se for "olhar" para a posição
  if jogs[jog].cmd = 'olhar' then mandarPos(jog)
  //testar por saídas
  else if TentarSair(jog) then exit
  //lista de jogadores
  else if jogs[jog].cmd = 'quem' then MandarConectados(jog)
  //chat para todos os jogadores
  else if str.strings[0] = 'falar' then falar(jog)
  //chat para um jogador
  else if str.strings[0] = 'dizer' then dizer(jog, str)
  //pegar um objeto do cenário
  else if str.strings[0] = 'pegar' then pegar(jog, str)
  //mostrar conteúdo do inventário
  else if jogs[jog].cmd = 'inventario' then mostrarInv(jog)
  //soltar um objeto do inventário
  else if str.strings[0] = 'soltar' then soltar(jog);

Pronto! O sistema de objetos foi criado.

Conclusões

Tendo completado esse artigo, você terá os elementos básicos de um servidor MUD, genérico o suficiente para permitir qualquer ambientação ou tema para seu jogo.

Jogue alguns monstros, crie mais locais e você terá um mundo com uma capacidade incrível de incitar a imaginação de até milhares de pessoas. Desenvolva seu projeto e teremos enorme prazer em mostrá-lo e divulgá-lo.

Bom divertimento.


Download...
Clique no link para fazer o download dos arquivos. Se sua assinatura do club TILT está para vencer, clique aqui e saiba como renová-la.

Fontes completos do exemplo da matéria
 
online